diff --git a/docs/src/further_topics/lenient_maths.rst b/docs/src/further_topics/lenient_maths.rst index 4aad721780..643bd37e76 100644 --- a/docs/src/further_topics/lenient_maths.rst +++ b/docs/src/further_topics/lenient_maths.rst @@ -68,26 +68,26 @@ represents the output of an low-resolution global atmospheric ``experiment``, .. doctest:: lenient-example >>> print(experiment) - air_potential_temperature / (K) (model_level_number: 15; grid_latitude: 100; grid_longitude: 100) - Dimension coordinates: - model_level_number x - - - grid_latitude - x - - grid_longitude - - x - Auxiliary coordinates: - atmosphere_hybrid_height_coordinate x - - - sigma x - - - surface_altitude - x x - Derived coordinates: - altitude x x x - Scalar coordinates: - forecast_period: 0.0 hours - forecast_reference_time: 2009-09-09 17:10:00 - time: 2009-09-09 17:10:00 - Attributes: - Conventions: CF-1.5 - STASH: m01s00i004 - experiment-id: RT3 50 - source: Data from Met Office Unified Model 7.04 + air_potential_temperature / (K) (model_level_number: 15; grid_latitude: 100; grid_longitude: 100) + Dimension coordinates: + model_level_number x - - + grid_latitude - x - + grid_longitude - - x + Auxiliary coordinates: + atmosphere_hybrid_height_coordinate x - - + sigma x - - + surface_altitude - x x + Derived coordinates: + altitude x x x + Scalar coordinates: + forecast_period 0.0 hours + forecast_reference_time 2009-09-09 17:10:00 + time 2009-09-09 17:10:00 + Attributes: + Conventions CF-1.5 + STASH m01s00i004 + experiment-id RT3 50 + source Data from Met Office Unified Model 7.04 Consider also the following :class:`~iris.cube.Cube`, which has the same global spatial extent, and acts as a ``control``, @@ -96,16 +96,16 @@ spatial extent, and acts as a ``control``, >>> print(control) air_potential_temperature / (K) (grid_latitude: 100; grid_longitude: 100) - Dimension coordinates: - grid_latitude x - - grid_longitude - x - Scalar coordinates: - model_level_number: 1 - time: 2009-09-09 17:10:00 - Attributes: - Conventions: CF-1.7 - STASH: m01s00i004 - source: Data from Met Office Unified Model 7.04 + Dimension coordinates: + grid_latitude x - + grid_longitude - x + Scalar coordinates: + model_level_number 1 + time 2009-09-09 17:10:00 + Attributes: + Conventions CF-1.7 + STASH m01s00i004 + source Data from Met Office Unified Model 7.04 Now let's subtract these cubes in order to calculate a simple ``difference``, @@ -113,24 +113,24 @@ Now let's subtract these cubes in order to calculate a simple ``difference``, >>> difference = experiment - control >>> print(difference) - unknown / (K) (model_level_number: 15; grid_latitude: 100; grid_longitude: 100) - Dimension coordinates: - model_level_number x - - - grid_latitude - x - - grid_longitude - - x - Auxiliary coordinates: - atmosphere_hybrid_height_coordinate x - - - sigma x - - - surface_altitude - x x - Derived coordinates: - altitude x x x - Scalar coordinates: - forecast_period: 0.0 hours - forecast_reference_time: 2009-09-09 17:10:00 - time: 2009-09-09 17:10:00 - Attributes: - experiment-id: RT3 50 - source: Data from Met Office Unified Model 7.04 + unknown / (K) (model_level_number: 15; grid_latitude: 100; grid_longitude: 100) + Dimension coordinates: + model_level_number x - - + grid_latitude - x - + grid_longitude - - x + Auxiliary coordinates: + atmosphere_hybrid_height_coordinate x - - + sigma x - - + surface_altitude - x x + Derived coordinates: + altitude x x x + Scalar coordinates: + forecast_period 0.0 hours + forecast_reference_time 2009-09-09 17:10:00 + time 2009-09-09 17:10:00 + Attributes: + experiment-id RT3 50 + source Data from Met Office Unified Model 7.04 Note that, cube maths automatically takes care of broadcasting the dimensionality of the ``control`` up to that of the ``experiment``, in order to @@ -204,21 +204,21 @@ time perform **strict** cube maths instead, ... difference = experiment - control ... >>> print(difference) - unknown / (K) (model_level_number: 15; grid_latitude: 100; grid_longitude: 100) - Dimension coordinates: - model_level_number x - - - grid_latitude - x - - grid_longitude - - x - Auxiliary coordinates: - atmosphere_hybrid_height_coordinate x - - - sigma x - - - surface_altitude - x x - Derived coordinates: - altitude x x x - Scalar coordinates: - time: 2009-09-09 17:10:00 - Attributes: - source: Data from Met Office Unified Model 7.04 + unknown / (K) (model_level_number: 15; grid_latitude: 100; grid_longitude: 100) + Dimension coordinates: + model_level_number x - - + grid_latitude - x - + grid_longitude - - x + Auxiliary coordinates: + atmosphere_hybrid_height_coordinate x - - + sigma x - - + surface_altitude - x x + Derived coordinates: + altitude x x x + Scalar coordinates: + time 2009-09-09 17:10:00 + Attributes: + source Data from Met Office Unified Model 7.04 Although the numerical result of this strict cube maths operation is identical, it is not as rich in metadata as the :ref:`lenient alternative `. diff --git a/docs/src/further_topics/metadata.rst b/docs/src/further_topics/metadata.rst index ab6a6450b4..79e9c164a0 100644 --- a/docs/src/further_topics/metadata.rst +++ b/docs/src/further_topics/metadata.rst @@ -108,22 +108,22 @@ For example, given the following :class:`~iris.cube.Cube`, >>> print(cube) air_temperature / (K) (time: 240; latitude: 37; longitude: 49) - Dimension coordinates: - time x - - - latitude - x - - longitude - - x - Auxiliary coordinates: - forecast_period x - - - Scalar coordinates: - forecast_reference_time: 1859-09-01 06:00:00 - height: 1.5 m - Attributes: - Conventions: CF-1.5 - Model scenario: A1B - STASH: m01s03i236 - source: Data from Met Office Unified Model 6.05 - Cell methods: - mean: time (6 hour) + Dimension coordinates: + time x - - + latitude - x - + longitude - - x + Auxiliary coordinates: + forecast_period x - - + Scalar coordinates: + forecast_reference_time 1859-09-01 06:00:00 + height 1.5 m + Cell methods: + mean time (6 hour) + Attributes: + Conventions CF-1.5 + Model scenario A1B + STASH m01s03i236 + source Data from Met Office Unified Model 6.05 We can easily get all of the associated metadata of the :class:`~iris.cube.Cube` using the ``metadata`` property: diff --git a/docs/src/userguide/cube_maths.rst b/docs/src/userguide/cube_maths.rst index 35e672eaf6..78490cd749 100644 --- a/docs/src/userguide/cube_maths.rst +++ b/docs/src/userguide/cube_maths.rst @@ -56,16 +56,16 @@ but with the data representing their difference: >>> print(t_last - t_first) unknown / (K) (latitude: 37; longitude: 49) - Dimension coordinates: - latitude x - - longitude - x - Scalar coordinates: - forecast_reference_time: 1859-09-01 06:00:00 - height: 1.5 m - Attributes: - Conventions: CF-1.5 - Model scenario: E1 - source: Data from Met Office Unified Model 6.05 + Dimension coordinates: + latitude x - + longitude - x + Scalar coordinates: + forecast_reference_time 1859-09-01 06:00:00 + height 1.5 m + Attributes: + Conventions CF-1.5 + Model scenario E1 + source Data from Met Office Unified Model 6.05 .. note:: diff --git a/docs/src/userguide/cube_statistics.rst b/docs/src/userguide/cube_statistics.rst index 4eb016078e..ac66ff4e53 100644 --- a/docs/src/userguide/cube_statistics.rst +++ b/docs/src/userguide/cube_statistics.rst @@ -23,9 +23,9 @@ Collapsing Entire Data Dimensions In the :doc:`subsetting_a_cube` section we saw how to extract a subset of a cube in order to reduce either its dimensionality or its resolution. -Instead of simply extracting a sub-region of the data, -we can produce statistical functions of the data values -across a particular dimension, +Instead of simply extracting a sub-region of the data, +we can produce statistical functions of the data values +across a particular dimension, such as a 'mean over time' or 'minimum over latitude'. .. _cube-statistics_forecast_printout: @@ -37,59 +37,59 @@ For instance, suppose we have a cube: >>> cube = iris.load_cube(filename, 'air_potential_temperature') >>> print(cube) air_potential_temperature / (K) (time: 3; model_level_number: 7; grid_latitude: 204; grid_longitude: 187) - Dimension coordinates: - time x - - - - model_level_number - x - - - grid_latitude - - x - - grid_longitude - - - x - Auxiliary coordinates: - forecast_period x - - - - level_height - x - - - sigma - x - - - surface_altitude - - x x - Derived coordinates: - altitude - x x x - Scalar coordinates: - forecast_reference_time: 2009-11-19 04:00:00 - Attributes: - STASH: m01s00i004 - source: Data from Met Office Unified Model - um_version: 7.3 - - -In this case we have a 4 dimensional cube; -to mean the vertical (z) dimension down to a single valued extent -we can pass the coordinate name and the aggregation definition to the + Dimension coordinates: + time x - - - + model_level_number - x - - + grid_latitude - - x - + grid_longitude - - - x + Auxiliary coordinates: + forecast_period x - - - + level_height - x - - + sigma - x - - + surface_altitude - - x x + Derived coordinates: + altitude - x x x + Scalar coordinates: + forecast_reference_time 2009-11-19 04:00:00 + Attributes: + STASH m01s00i004 + source Data from Met Office Unified Model + um_version 7.3 + + +In this case we have a 4 dimensional cube; +to mean the vertical (z) dimension down to a single valued extent +we can pass the coordinate name and the aggregation definition to the :meth:`Cube.collapsed() ` method: >>> import iris.analysis >>> vertical_mean = cube.collapsed('model_level_number', iris.analysis.MEAN) >>> print(vertical_mean) air_potential_temperature / (K) (time: 3; grid_latitude: 204; grid_longitude: 187) - Dimension coordinates: - time x - - - grid_latitude - x - - grid_longitude - - x - Auxiliary coordinates: - forecast_period x - - - surface_altitude - x x - Derived coordinates: - altitude - x x - Scalar coordinates: - forecast_reference_time: 2009-11-19 04:00:00 - level_height: 696.6666 m, bound=(0.0, 1393.3333) m - model_level_number: 10, bound=(1, 19) - sigma: 0.92292976, bound=(0.8458596, 1.0) - Attributes: - STASH: m01s00i004 - source: Data from Met Office Unified Model - um_version: 7.3 - Cell methods: - mean: model_level_number - - -Similarly other analysis operators such as ``MAX``, ``MIN`` and ``STD_DEV`` -can be used instead of ``MEAN``, see :mod:`iris.analysis` for a full list + Dimension coordinates: + time x - - + grid_latitude - x - + grid_longitude - - x + Auxiliary coordinates: + forecast_period x - - + surface_altitude - x x + Derived coordinates: + altitude - x x + Scalar coordinates: + forecast_reference_time 2009-11-19 04:00:00 + level_height 696.6666 m, bound=(0.0, 1393.3333) m + model_level_number 10, bound=(1, 19) + sigma 0.92292976, bound=(0.8458596, 1.0) + Cell methods: + mean model_level_number + Attributes: + STASH m01s00i004 + source Data from Met Office Unified Model + um_version 7.3 + + +Similarly other analysis operators such as ``MAX``, ``MIN`` and ``STD_DEV`` +can be used instead of ``MEAN``, see :mod:`iris.analysis` for a full list of currently supported operators. For an example of using this functionality, the @@ -103,14 +103,14 @@ in the gallery takes a zonal mean of an ``XYT`` cube by using the Area Averaging ^^^^^^^^^^^^^^ -Some operators support additional keywords to the ``cube.collapsed`` method. -For example, :func:`iris.analysis.MEAN ` supports -a weights keyword which can be combined with +Some operators support additional keywords to the ``cube.collapsed`` method. +For example, :func:`iris.analysis.MEAN ` supports +a weights keyword which can be combined with :func:`iris.analysis.cartography.area_weights` to calculate an area average. -Let's use the same data as was loaded in the previous example. -Since ``grid_latitude`` and ``grid_longitude`` were both point coordinates -we must guess bound positions for them +Let's use the same data as was loaded in the previous example. +Since ``grid_latitude`` and ``grid_longitude`` were both point coordinates +we must guess bound positions for them in order to calculate the area of the grid boxes:: import iris.analysis.cartography @@ -125,26 +125,26 @@ These areas can now be passed to the ``collapsed`` method as weights: >>> new_cube = cube.collapsed(['grid_longitude', 'grid_latitude'], iris.analysis.MEAN, weights=grid_areas) >>> print(new_cube) air_potential_temperature / (K) (time: 3; model_level_number: 7) - Dimension coordinates: - time x - - model_level_number - x - Auxiliary coordinates: - forecast_period x - - level_height - x - sigma - x - Derived coordinates: - altitude - x - Scalar coordinates: - forecast_reference_time: 2009-11-19 04:00:00 - grid_latitude: 1.5145501 degrees, bound=(0.14430022, 2.8848) degrees - grid_longitude: 358.74948 degrees, bound=(357.494, 360.00497) degrees - surface_altitude: 399.625 m, bound=(-14.0, 813.25) m - Attributes: - STASH: m01s00i004 - source: Data from Met Office Unified Model - um_version: 7.3 - Cell methods: - mean: grid_longitude, grid_latitude + Dimension coordinates: + time x - + model_level_number - x + Auxiliary coordinates: + forecast_period x - + level_height - x + sigma - x + Derived coordinates: + altitude - x + Scalar coordinates: + forecast_reference_time 2009-11-19 04:00:00 + grid_latitude 1.5145501 degrees, bound=(0.14430022, 2.8848) degrees + grid_longitude 358.74948 degrees, bound=(357.494, 360.00497) degrees + surface_altitude 399.625 m, bound=(-14.0, 813.25) m + Cell methods: + mean grid_longitude, grid_latitude + Attributes: + STASH m01s00i004 + source Data from Met Office Unified Model + um_version 7.3 Several examples of area averaging exist in the gallery which may be of interest, including an example on taking a :ref:`global area-weighted mean @@ -155,24 +155,24 @@ including an example on taking a :ref:`global area-weighted mean Partially Reducing Data Dimensions ---------------------------------- -Instead of completely collapsing a dimension, other methods can be applied -to reduce or filter the number of data points of a particular dimension. +Instead of completely collapsing a dimension, other methods can be applied +to reduce or filter the number of data points of a particular dimension. Aggregation of Grouped Data ^^^^^^^^^^^^^^^^^^^^^^^^^^^ -The :meth:`Cube.aggregated_by ` operation -combines data for all points with the same value of a given coordinate. -To do this, you need a coordinate whose points take on only a limited set -of different values -- the *number* of these then determines the size of the +The :meth:`Cube.aggregated_by ` operation +combines data for all points with the same value of a given coordinate. +To do this, you need a coordinate whose points take on only a limited set +of different values -- the *number* of these then determines the size of the reduced dimension. -The :mod:`iris.coord_categorisation` module can be used to make such -'categorical' coordinates out of ordinary ones: The most common use is -to aggregate data over regular *time intervals*, +The :mod:`iris.coord_categorisation` module can be used to make such +'categorical' coordinates out of ordinary ones: The most common use is +to aggregate data over regular *time intervals*, such as by calendar month or day of the week. -For example, let's create two new coordinates on the cube +For example, let's create two new coordinates on the cube to represent the climatological seasons and the season year respectively:: import iris @@ -188,8 +188,8 @@ to represent the climatological seasons and the season year respectively:: .. note:: - The 'season year' is not the same as year number, because (e.g.) the months - Dec11, Jan12 + Feb12 all belong to 'DJF-12'. + The 'season year' is not the same as year number, because (e.g.) the months + Dec11, Jan12 + Feb12 all belong to 'DJF-12'. See :meth:`iris.coord_categorisation.add_season_year`. @@ -206,31 +206,31 @@ to represent the climatological seasons and the season year respectively:: iris.coord_categorisation.add_season_year(cube, 'time', name='season_year') annual_seasonal_mean = cube.aggregated_by( - ['clim_season', 'season_year'], + ['clim_season', 'season_year'], iris.analysis.MEAN) - + Printing this cube now shows that two extra coordinates exist on the cube: .. doctest:: aggregation >>> print(cube) surface_temperature / (K) (time: 54; latitude: 18; longitude: 432) - Dimension coordinates: - time x - - - latitude - x - - longitude - - x - Auxiliary coordinates: - clim_season x - - - forecast_reference_time x - - - season_year x - - - Scalar coordinates: - forecast_period: 0 hours - Attributes: - Conventions: CF-1.5 - STASH: m01s00i024 - Cell methods: - mean: month, year + Dimension coordinates: + time x - - + latitude - x - + longitude - - x + Auxiliary coordinates: + clim_season x - - + forecast_reference_time x - - + season_year x - - + Scalar coordinates: + forecast_period 0 hours + Cell methods: + mean month, year + Attributes: + Conventions CF-1.5 + STASH m01s00i024 These two coordinates can now be used to aggregate by season and climate-year: @@ -238,20 +238,20 @@ These two coordinates can now be used to aggregate by season and climate-year: .. doctest:: aggregation >>> annual_seasonal_mean = cube.aggregated_by( - ... ['clim_season', 'season_year'], + ... ['clim_season', 'season_year'], ... iris.analysis.MEAN) >>> print(repr(annual_seasonal_mean)) - -The primary change in the cube is that the cube's data has been -reduced in the 'time' dimension by aggregation (taking means, in this case). -This has collected together all data points with the same values of season and + +The primary change in the cube is that the cube's data has been +reduced in the 'time' dimension by aggregation (taking means, in this case). +This has collected together all data points with the same values of season and season-year. The results are now indexed by the 19 different possible values of season and season-year in a new, reduced 'time' dimension. -We can see this by printing the first 10 values of season+year -from the original cube: These points are individual months, +We can see this by printing the first 10 values of season+year +from the original cube: These points are individual months, so adjacent ones are often in the same season: .. doctest:: aggregation @@ -271,7 +271,7 @@ so adjacent ones are often in the same season: djf 2007 djf 2007 -Compare this with the first 10 values of the new cube's coordinates: +Compare this with the first 10 values of the new cube's coordinates: All the points now have distinct season+year values: .. doctest:: aggregation @@ -294,7 +294,7 @@ All the points now have distinct season+year values: Because the original data started in April 2006 we have some incomplete seasons (e.g. there were only two months worth of data for 'mam-2006'). -In this case we can fix this by removing all of the resultant 'times' which +In this case we can fix this by removing all of the resultant 'times' which do not cover a three month period (note: judged here as > 3*28 days): .. doctest:: aggregation @@ -306,7 +306,7 @@ do not cover a three month period (note: judged here as > 3*28 days): >>> full_season_means -The final result now represents the seasonal mean temperature for 17 seasons +The final result now represents the seasonal mean temperature for 17 seasons from jja-2006 to jja-2010: .. doctest:: aggregation diff --git a/docs/src/userguide/interpolation_and_regridding.rst b/docs/src/userguide/interpolation_and_regridding.rst index 5a5a985ccb..2295bdd589 100644 --- a/docs/src/userguide/interpolation_and_regridding.rst +++ b/docs/src/userguide/interpolation_and_regridding.rst @@ -66,39 +66,39 @@ Let's take the air temperature cube we've seen previously: >>> air_temp = iris.load_cube(iris.sample_data_path('air_temp.pp')) >>> print(air_temp) air_temperature / (K) (latitude: 73; longitude: 96) - Dimension coordinates: - latitude x - - longitude - x - Scalar coordinates: - forecast_period: 6477 hours, bound=(-28083.0, 6477.0) hours - forecast_reference_time: 1998-03-01 03:00:00 - pressure: 1000.0 hPa - time: 1998-12-01 00:00:00, bound=(1994-12-01 00:00:00, 1998-12-01 00:00:00) - Attributes: - STASH: m01s16i203 - source: Data from Met Office Unified Model - Cell methods: - mean within years: time - mean over years: time + Dimension coordinates: + latitude x - + longitude - x + Scalar coordinates: + forecast_period 6477 hours, bound=(-28083.0, 6477.0) hours + forecast_reference_time 1998-03-01 03:00:00 + pressure 1000.0 hPa + time 1998-12-01 00:00:00, bound=(1994-12-01 00:00:00, 1998-12-01 00:00:00) + Cell methods: + mean within years time + mean over years time + Attributes: + STASH m01s16i203 + source Data from Met Office Unified Model We can interpolate specific values from the coordinates of the cube: >>> sample_points = [('latitude', 51.48), ('longitude', 0)] >>> print(air_temp.interpolate(sample_points, iris.analysis.Linear())) air_temperature / (K) (scalar cube) - Scalar coordinates: - forecast_period: 6477 hours, bound=(-28083.0, 6477.0) hours - forecast_reference_time: 1998-03-01 03:00:00 - latitude: 51.48 degrees - longitude: 0 degrees - pressure: 1000.0 hPa - time: 1998-12-01 00:00:00, bound=(1994-12-01 00:00:00, 1998-12-01 00:00:00) - Attributes: - STASH: m01s16i203 - source: Data from Met Office Unified Model - Cell methods: - mean within years: time - mean over years: time + Scalar coordinates: + forecast_period 6477 hours, bound=(-28083.0, 6477.0) hours + forecast_reference_time 1998-03-01 03:00:00 + latitude 51.48 degrees + longitude 0 degrees + pressure 1000.0 hPa + time 1998-12-01 00:00:00, bound=(1994-12-01 00:00:00, 1998-12-01 00:00:00) + Cell methods: + mean within years time + mean over years time + Attributes: + STASH m01s16i203 + source Data from Met Office Unified Model As we can see, the resulting cube is scalar and has longitude and latitude coordinates with the values defined in our sample points. @@ -237,12 +237,12 @@ For example:: Regridding ---------- -Regridding is conceptually a very similar process to interpolation in Iris. +Regridding is conceptually a very similar process to interpolation in Iris. The primary difference is that interpolation is based on sample points, while regridding is based on the **horizontal** grid of *another cube*. Regridding a cube is achieved with the :meth:`cube.regrid() ` method. -This method expects two arguments: +This method expects two arguments: #. *another cube* that defines the target grid onto which the cube should be regridded, and #. the regridding scheme to use. diff --git a/docs/src/userguide/iris_cubes.rst b/docs/src/userguide/iris_cubes.rst index de206486d3..64a9bfd822 100644 --- a/docs/src/userguide/iris_cubes.rst +++ b/docs/src/userguide/iris_cubes.rst @@ -156,24 +156,24 @@ output as this is the quickest way of inspecting the contents of a cube. Here is .. testoutput:: air_potential_temperature / (K) (time: 3; model_level_number: 7; grid_latitude: 204; grid_longitude: 187) - Dimension coordinates: - time x - - - - model_level_number - x - - - grid_latitude - - x - - grid_longitude - - - x - Auxiliary coordinates: - forecast_period x - - - - level_height - x - - - sigma - x - - - surface_altitude - - x x - Derived coordinates: - altitude - x x x - Scalar coordinates: - forecast_reference_time: 2009-11-19 04:00:00 - Attributes: - STASH: m01s00i004 - source: Data from Met Office Unified Model - um_version: 7.3 + Dimension coordinates: + time x - - - + model_level_number - x - - + grid_latitude - - x - + grid_longitude - - - x + Auxiliary coordinates: + forecast_period x - - - + level_height - x - - + sigma - x - - + surface_altitude - - x x + Derived coordinates: + altitude - x x x + Scalar coordinates: + forecast_reference_time 2009-11-19 04:00:00 + Attributes: + STASH m01s00i004 + source Data from Met Office Unified Model + um_version 7.3 Using this output we can deduce that: diff --git a/docs/src/userguide/loading_iris_cubes.rst b/docs/src/userguide/loading_iris_cubes.rst index 659c28420a..37c8fc3c12 100644 --- a/docs/src/userguide/loading_iris_cubes.rst +++ b/docs/src/userguide/loading_iris_cubes.rst @@ -4,23 +4,23 @@ Loading Iris Cubes =================== -To load a single file into a **list** of Iris cubes +To load a single file into a **list** of Iris cubes the :py:func:`iris.load` function is used:: import iris filename = '/path/to/file' cubes = iris.load(filename) -Iris will attempt to return **as few cubes as possible** -by collecting together multiple fields with a shared standard name -into a single multidimensional cube. +Iris will attempt to return **as few cubes as possible** +by collecting together multiple fields with a shared standard name +into a single multidimensional cube. -The :py:func:`iris.load` function automatically recognises the format +The :py:func:`iris.load` function automatically recognises the format of the given files and attempts to produce Iris Cubes from their contents. .. note:: - Currently there is support for CF NetCDF, GRIB 1 & 2, PP and FieldsFiles + Currently there is support for CF NetCDF, GRIB 1 & 2, PP and FieldsFiles file formats with a framework for this to be extended to custom formats. @@ -34,26 +34,26 @@ In order to find out what has been loaded, the result can be printed: 1: surface_altitude / (m) (grid_latitude: 204; grid_longitude: 187) -This shows that there were 2 cubes as a result of loading the file, they were: +This shows that there were 2 cubes as a result of loading the file, they were: ``air_potential_temperature`` and ``surface_altitude``. The ``surface_altitude`` cube was 2 dimensional with: - * the two dimensions have extents of 204 and 187 respectively and are + * the two dimensions have extents of 204 and 187 respectively and are represented by the ``grid_latitude`` and ``grid_longitude`` coordinates. The ``air_potential_temperature`` cubes were 4 dimensional with: - * the same length ``grid_latitude`` and ``grid_longitude`` dimensions as + * the same length ``grid_latitude`` and ``grid_longitude`` dimensions as ``surface_altitide`` * a ``time`` dimension of length 3 * a ``model_level_number`` dimension of length 7 .. note:: - The result of :func:`iris.load` is **always** a - :class:`list of cubes `. - Anything that can be done with a Python :class:`list` can be done + The result of :func:`iris.load` is **always** a + :class:`list of cubes `. + Anything that can be done with a Python :class:`list` can be done with the resultant list of cubes. It is worth noting, however, that there is no inherent order to this :class:`list of cubes `. @@ -63,19 +63,19 @@ The ``air_potential_temperature`` cubes were 4 dimensional with: .. hint:: - Throughout this user guide you will see the function - ``iris.sample_data_path`` being used to get the filename for the resources + Throughout this user guide you will see the function + ``iris.sample_data_path`` being used to get the filename for the resources used in the examples. The result of this function is just a string. - - Using this function allows us to provide examples which will work - across platforms and with data installed in different locations, + + Using this function allows us to provide examples which will work + across platforms and with data installed in different locations, however in practice you will want to use your own strings:: - + filename = '/path/to/file' cubes = iris.load(filename) -To get the air potential temperature cube from the list of cubes -returned by :py:func:`iris.load` in the previous example, +To get the air potential temperature cube from the list of cubes +returned by :py:func:`iris.load` in the previous example, list indexing can be used: >>> import iris @@ -85,41 +85,41 @@ list indexing can be used: >>> air_potential_temperature = cubes[0] >>> print(air_potential_temperature) air_potential_temperature / (K) (time: 3; model_level_number: 7; grid_latitude: 204; grid_longitude: 187) - Dimension coordinates: - time x - - - - model_level_number - x - - - grid_latitude - - x - - grid_longitude - - - x - Auxiliary coordinates: - forecast_period x - - - - level_height - x - - - sigma - x - - - surface_altitude - - x x - Derived coordinates: - altitude - x x x - Scalar coordinates: - forecast_reference_time: 2009-11-19 04:00:00 - Attributes: - STASH: m01s00i004 - source: Data from Met Office Unified Model - um_version: 7.3 - -Notice that the result of printing a **cube** is a little more verbose than -it was when printing a **list of cubes**. In addition to the very short summary -which is provided when printing a list of cubes, information is provided -on the coordinates which constitute the cube in question. + Dimension coordinates: + time x - - - + model_level_number - x - - + grid_latitude - - x - + grid_longitude - - - x + Auxiliary coordinates: + forecast_period x - - - + level_height - x - - + sigma - x - - + surface_altitude - - x x + Derived coordinates: + altitude - x x x + Scalar coordinates: + forecast_reference_time 2009-11-19 04:00:00 + Attributes: + STASH m01s00i004 + source Data from Met Office Unified Model + um_version 7.3 + +Notice that the result of printing a **cube** is a little more verbose than +it was when printing a **list of cubes**. In addition to the very short summary +which is provided when printing a list of cubes, information is provided +on the coordinates which constitute the cube in question. This was the output discussed at the end of the :doc:`iris_cubes` section. .. note:: - Dimensioned coordinates will have a dimension marker ``x`` in the - appropriate column for each cube data dimension that they describe. + Dimensioned coordinates will have a dimension marker ``x`` in the + appropriate column for each cube data dimension that they describe. Loading Multiple Files ----------------------- -To load more than one file into a list of cubes, a list of filenames can be +To load more than one file into a list of cubes, a list of filenames can be provided to :py:func:`iris.load`:: filenames = [iris.sample_data_path('uk_hires.pp'), @@ -127,10 +127,10 @@ provided to :py:func:`iris.load`:: cubes = iris.load(filenames) -It is also possible to load one or more files with wildcard substitution +It is also possible to load one or more files with wildcard substitution using the expansion rules defined :py:mod:`fnmatch`. -For example, to match **zero or more characters** in the filename, +For example, to match **zero or more characters** in the filename, star wildcards can be used:: filename = iris.sample_data_path('GloSea4', '*.pp') @@ -139,7 +139,7 @@ star wildcards can be used:: .. note:: - The cubes returned will not necessarily be in the same order as the + The cubes returned will not necessarily be in the same order as the order of the filenames. Lazy Loading @@ -157,9 +157,9 @@ For more on the benefits, handling and uses of lazy data, see :doc:`Real and Laz Constrained Loading ----------------------- -Given a large dataset, it is possible to restrict or constrain the load -to match specific Iris cube metadata. -Constrained loading provides the ability to generate a cube +Given a large dataset, it is possible to restrict or constrain the load +to match specific Iris cube metadata. +Constrained loading provides the ability to generate a cube from a specific subset of data that is of particular interest. As we have seen, loading the following file creates several Cubes:: @@ -167,7 +167,7 @@ As we have seen, loading the following file creates several Cubes:: filename = iris.sample_data_path('uk_hires.pp') cubes = iris.load(filename) -Specifying a name as a constraint argument to :py:func:`iris.load` will mean +Specifying a name as a constraint argument to :py:func:`iris.load` will mean only cubes with matching :meth:`name ` will be returned:: @@ -193,21 +193,21 @@ of ``m01s00i033``:: cubes = iris.load(filename, constraint) To constrain the load to multiple distinct constraints, a list of constraints -can be provided. This is equivalent to running load once for each constraint +can be provided. This is equivalent to running load once for each constraint but is likely to be more efficient:: filename = iris.sample_data_path('uk_hires.pp') cubes = iris.load(filename, ['air_potential_temperature', 'surface_altitude']) -The :class:`iris.Constraint` class can be used to restrict coordinate values -on load. For example, to constrain the load to match +The :class:`iris.Constraint` class can be used to restrict coordinate values +on load. For example, to constrain the load to match a specific ``model_level_number``:: filename = iris.sample_data_path('uk_hires.pp') level_10 = iris.Constraint(model_level_number=10) cubes = iris.load(filename, level_10) -Constraints can be combined using ``&`` to represent a more restrictive +Constraints can be combined using ``&`` to represent a more restrictive constraint to ``load``:: filename = iris.sample_data_path('uk_hires.pp') @@ -215,16 +215,16 @@ constraint to ``load``:: level_10 = iris.Constraint(model_level_number=10) cubes = iris.load(filename, forecast_6 & level_10) -As well as being able to combine constraints using ``&``, -the :class:`iris.Constraint` class can accept multiple arguments, -and a list of values can be given to constrain a coordinate to one of +As well as being able to combine constraints using ``&``, +the :class:`iris.Constraint` class can accept multiple arguments, +and a list of values can be given to constrain a coordinate to one of a collection of values:: filename = iris.sample_data_path('uk_hires.pp') level_10_or_16_fp_6 = iris.Constraint(model_level_number=[10, 16], forecast_period=6) cubes = iris.load(filename, level_10_or_16_fp_6) -A common requirement is to limit the value of a coordinate to a specific range, +A common requirement is to limit the value of a coordinate to a specific range, this can be achieved by passing the constraint a function:: def bottom_16_levels(cell): @@ -234,11 +234,11 @@ this can be achieved by passing the constraint a function:: filename = iris.sample_data_path('uk_hires.pp') level_lt_16 = iris.Constraint(model_level_number=bottom_16_levels) cubes = iris.load(filename, level_lt_16) - + .. note:: - As with many of the examples later in this documentation, the - simple function above can be conveniently written as a lambda function + As with many of the examples later in this documentation, the + simple function above can be conveniently written as a lambda function on a single line:: bottom_16_levels = lambda cell: cell <= 16 @@ -247,8 +247,8 @@ this can be achieved by passing the constraint a function:: Note also the :ref:`warning on equality constraints with floating point coordinates `. -Cube attributes can also be part of the constraint criteria. Supposing a -cube attribute of ``STASH`` existed, as is the case when loading ``PP`` files, +Cube attributes can also be part of the constraint criteria. Supposing a +cube attribute of ``STASH`` existed, as is the case when loading ``PP`` files, then specific STASH codes can be filtered:: filename = iris.sample_data_path('uk_hires.pp') @@ -257,8 +257,8 @@ then specific STASH codes can be filtered:: .. seealso:: - For advanced usage there are further examples in the - :class:`iris.Constraint` reference documentation. + For advanced usage there are further examples in the + :class:`iris.Constraint` reference documentation. Constraining a Circular Coordinate Across its Boundary @@ -390,7 +390,7 @@ PartialDateTime this becomes simple: >>> st_swithuns_daterange = iris.Constraint( ... time=lambda cell: PartialDateTime(month=7, day=15) <= cell < PartialDateTime(month=8, day=25)) >>> within_st_swithuns = long_ts.extract(st_swithuns_daterange) - ... + ... >>> print(within_st_swithuns.coord('time')) DimCoord([2007-07-16 00:00:00, 2007-07-23 00:00:00, 2007-07-30 00:00:00, 2007-08-06 00:00:00, 2007-08-13 00:00:00, 2007-08-20 00:00:00, @@ -407,13 +407,13 @@ Strict Loading -------------- The :py:func:`iris.load_cube` and :py:func:`iris.load_cubes` functions are -similar to :py:func:`iris.load` except they can only return +similar to :py:func:`iris.load` except they can only return *one cube per constraint*. The :func:`iris.load_cube` function accepts a single constraint and returns a single cube. The :func:`iris.load_cubes` function accepts any number of constraints and returns a list of cubes (as an `iris.cube.CubeList`). Providing no constraints to :func:`iris.load_cube` or :func:`iris.load_cubes` -is equivalent to requesting exactly one cube of any type. +is equivalent to requesting exactly one cube of any type. A single cube is loaded in the following example:: @@ -426,9 +426,9 @@ A single cube is loaded in the following example:: longitude - x ... Cell methods: - mean: time + mean time -However, when attempting to load data which would result in anything other than +However, when attempting to load data which would result in anything other than one cube, an exception is raised:: >>> filename = iris.sample_data_path('uk_hires.pp') @@ -438,19 +438,19 @@ one cube, an exception is raised:: iris.exceptions.ConstraintMismatchError: Expected exactly one cube, found 2. .. note:: - - All the load functions share many of the same features, hence - multiple files could be loaded with wildcard filenames + + All the load functions share many of the same features, hence + multiple files could be loaded with wildcard filenames or by providing a list of filenames. The strict nature of :func:`iris.load_cube` and :func:`iris.load_cubes` -means that, when combined with constrained loading, it is possible to -ensure that precisely what was asked for on load is given -- otherwise an exception is raised. -This fact can be utilised to make code only run successfully if +means that, when combined with constrained loading, it is possible to +ensure that precisely what was asked for on load is given +- otherwise an exception is raised. +This fact can be utilised to make code only run successfully if the data provided has the expected criteria. -For example, suppose that code needed ``air_potential_temperature`` +For example, suppose that code needed ``air_potential_temperature`` in order to run:: import iris @@ -458,23 +458,23 @@ in order to run:: air_pot_temp = iris.load_cube(filename, 'air_potential_temperature') print(air_pot_temp) -Should the file not produce exactly one cube with a standard name of +Should the file not produce exactly one cube with a standard name of 'air_potential_temperature', an exception will be raised. -Similarly, supposing a routine needed both 'surface_altitude' and +Similarly, supposing a routine needed both 'surface_altitude' and 'air_potential_temperature' to be able to run:: import iris filename = iris.sample_data_path('uk_hires.pp') altitude_cube, pot_temp_cube = iris.load_cubes(filename, ['surface_altitude', 'air_potential_temperature']) -The result of :func:`iris.load_cubes` in this case will be a list of 2 cubes -ordered by the constraints provided. Multiple assignment has been used to put +The result of :func:`iris.load_cubes` in this case will be a list of 2 cubes +ordered by the constraints provided. Multiple assignment has been used to put these two cubes into separate variables. .. note:: - In Python, lists of a pre-known length and order can be exploited + In Python, lists of a pre-known length and order can be exploited using *multiple assignment*: >>> number_one, number_two = [1, 2] diff --git a/docs/src/userguide/merge_and_concat.rst b/docs/src/userguide/merge_and_concat.rst index f98685398b..cc619fb7fd 100644 --- a/docs/src/userguide/merge_and_concat.rst +++ b/docs/src/userguide/merge_and_concat.rst @@ -108,18 +108,20 @@ make a new ``z`` dimension coordinate: >>> print(cubes[0]) air_temperature / (kelvin) (y: 4; x: 5) ... - Scalar coordinates: - z: 1 meters + Scalar coordinates: + z 1 meters >>> print(cubes[1]) air_temperature / (kelvin) (y: 4; x: 5) ... - Scalar coordinates: - z: 2 meters + Scalar coordinates: + z 2 meters >>> print(cubes[2]) air_temperature / (kelvin) (y: 4; x: 5) - ... - Scalar coordinates: - z: 3 meters + Dimension coordinates: + y x - + x - x + Scalar coordinates: + z 3 meters >>> print(cubes.merge()) 0: air_temperature / (kelvin) (z: 3; y: 4; x: 5) @@ -553,18 +555,18 @@ combine your cubes:: >>> print(cubes[0]) air_temperature / (kelvin) (y: 4; x: 5) - Dimension coordinates: - x x - - y - x - Scalar coordinates: - z: 1 + Dimension coordinates: + y x - + x - x + Scalar coordinates: + z 1 meters >>> print(cubes[1]) air_temperature / (kelvin) (y: 4; x: 5) - Dimension coordinates: - x x - - y - x - Scalar coordinates: - z: 2 + Dimension coordinates: + y x - + x - x + Scalar coordinates: + z 2 meters If your cubes are similar to those below (the single value ``z`` coordinate is diff --git a/docs/src/userguide/navigating_a_cube.rst b/docs/src/userguide/navigating_a_cube.rst index df18c032c1..74b47b258e 100644 --- a/docs/src/userguide/navigating_a_cube.rst +++ b/docs/src/userguide/navigating_a_cube.rst @@ -25,17 +25,17 @@ We have already seen a basic string representation of a cube when printing: >>> cube = iris.load_cube(filename) >>> print(cube) air_pressure_at_sea_level / (Pa) (grid_latitude: 22; grid_longitude: 36) - Dimension coordinates: - grid_latitude x - - grid_longitude - x - Scalar coordinates: - forecast_period: 0.0 hours - forecast_reference_time: 2006-06-15 00:00:00 - time: 2006-06-15 00:00:00 - Attributes: - Conventions: CF-1.5 - STASH: m01s16i222 - source: Data from Met Office Unified Model 6.01 + Dimension coordinates: + grid_latitude x - + grid_longitude - x + Scalar coordinates: + forecast_period 0.0 hours + forecast_reference_time 2006-06-15 00:00:00 + time 2006-06-15 00:00:00 + Attributes: + Conventions CF-1.5 + STASH m01s16i222 + source Data from Met Office Unified Model 6.01 This representation is equivalent to passing the cube to the :func:`str` function. This function can be used on @@ -160,18 +160,18 @@ We can add and remove coordinates via :func:`Cube.add_dim_coord>> cube.add_aux_coord(new_coord) >>> print(cube) air_pressure_at_sea_level / (Pa) (grid_latitude: 22; grid_longitude: 36) - Dimension coordinates: - grid_latitude x - - grid_longitude - x - Scalar coordinates: - forecast_period: 0.0 hours - forecast_reference_time: 2006-06-15 00:00:00 - my_custom_coordinate: 1 - time: 2006-06-15 00:00:00 - Attributes: - Conventions: CF-1.5 - STASH: m01s16i222 - source: Data from Met Office Unified Model 6.01 + Dimension coordinates: + grid_latitude x - + grid_longitude - x + Scalar coordinates: + forecast_period 0.0 hours + forecast_reference_time 2006-06-15 00:00:00 + my_custom_coordinate 1 + time 2006-06-15 00:00:00 + Attributes: + Conventions CF-1.5 + STASH m01s16i222 + source Data from Met Office Unified Model 6.01 The coordinate ``my_custom_coordinate`` now exists on the cube and is listed under the non-dimensioned single valued scalar coordinates. diff --git a/docs/src/userguide/subsetting_a_cube.rst b/docs/src/userguide/subsetting_a_cube.rst index 02cf1645a1..1c68cafb8d 100644 --- a/docs/src/userguide/subsetting_a_cube.rst +++ b/docs/src/userguide/subsetting_a_cube.rst @@ -21,16 +21,16 @@ A subset of a cube can be "extracted" from a multi-dimensional cube in order to >>> equator_slice = cube.extract(iris.Constraint(grid_latitude=0)) >>> print(equator_slice) electron density / (1E11 e/m^3) (height: 29; grid_longitude: 31) - Dimension coordinates: - height x - - grid_longitude - x - Auxiliary coordinates: - latitude - x - longitude - x - Scalar coordinates: - grid_latitude: 0.0 degrees - Attributes: - Conventions: CF-1.5 + Dimension coordinates: + height x - + grid_longitude - x + Auxiliary coordinates: + latitude - x + longitude - x + Scalar coordinates: + grid_latitude 0.0 degrees + Attributes: + Conventions CF-1.5 In this example we start with a 3 dimensional cube, with dimensions of ``height``, ``grid_latitude`` and ``grid_longitude``, @@ -81,24 +81,24 @@ same way as loading with constraints: 0: air_potential_temperature / (K) (grid_latitude: 204; grid_longitude: 187) >>> print(cubes[0]) air_potential_temperature / (K) (grid_latitude: 204; grid_longitude: 187) - Dimension coordinates: - grid_latitude x - - grid_longitude - x - Auxiliary coordinates: - surface_altitude x x - Derived coordinates: - altitude x x - Scalar coordinates: - forecast_period: 6.0 hours - forecast_reference_time: 2009-11-19 04:00:00 - level_height: 395.0 m, bound=(360.0, 433.3332) m - model_level_number: 10 - sigma: 0.9549927, bound=(0.9589389, 0.95068014) - time: 2009-11-19 10:00:00 - Attributes: - STASH: m01s00i004 - source: Data from Met Office Unified Model - um_version: 7.3 + Dimension coordinates: + grid_latitude x - + grid_longitude - x + Auxiliary coordinates: + surface_altitude x x + Derived coordinates: + altitude x x + Scalar coordinates: + forecast_period 6.0 hours + forecast_reference_time 2009-11-19 04:00:00 + level_height 395.0 m, bound=(360.0, 433.3332) m + model_level_number 10 + sigma 0.9549927, bound=(0.9589389, 0.95068014) + time 2009-11-19 10:00:00 + Attributes: + STASH m01s00i004 + source Data from Met Office Unified Model + um_version 7.3 Cube Iteration diff --git a/docs/src/whatsnew/latest.rst b/docs/src/whatsnew/latest.rst index de99f9a592..4daff5a2d1 100644 --- a/docs/src/whatsnew/latest.rst +++ b/docs/src/whatsnew/latest.rst @@ -56,6 +56,12 @@ This document explains the changes made to Iris for this release #. `@Badboy-16`_ implemented a ``CubeList.copy()`` method to return a ``CubeList`` object instead of a ``list``. (:pull:`4094`) +#. `@pp-mo`_ and `@trexfeathers`_ reformatted :meth:`iris.cube.Cube.summary`, + (which is used for ``print(Cube)``); putting + :attr:`~iris.cube.Cube.cell_methods` before + :attr:`~iris.cube.Cube.attributes`, and improving spacing throughout. + (:pull:`4206`) + 🐛 Bugs Fixed ============= @@ -220,9 +226,14 @@ This document explains the changes made to Iris for this release ``gallery`` tasks into a single task and associated `nox`_ session. (:pull:`4219`) -#. `@jamesp`_ and `@trexfeathers`_ implmented a benchmarking CI check +#. `@jamesp`_ and `@trexfeathers`_ implemented a benchmarking CI check using `asv`_. (:pull:`4253`) +#. `@pp-mo`_ refactored almost all of :meth:`iris.cube.Cube.summary` into the + new private module: :mod:`iris._representation`; rewritten with a more + modular approach, resulting in more readable and extensible code. + (:pull:`4206`) + .. comment Whatsnew author names (@github name) in alphabetical order. Note that, core dev names are automatically included by the common_links.inc: diff --git a/lib/iris/_representation/__init__.py b/lib/iris/_representation/__init__.py new file mode 100644 index 0000000000..f6c7fdf9b4 --- /dev/null +++ b/lib/iris/_representation/__init__.py @@ -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. + +""" diff --git a/lib/iris/_representation/cube_printout.py b/lib/iris/_representation/cube_printout.py new file mode 100644 index 0000000000..81d46bb29f --- /dev/null +++ b/lib/iris/_representation/cube_printout.py @@ -0,0 +1,356 @@ +# 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 + +from iris._representation.cube_summary import CubeSummary + + +class Table: + """ + A container of text strings in rows + columns, that can format its content + into a string per row, with contents in columns of fixed width. + + Supports left- or right- aligned columns, alignment being set "per row". + A column may also be set, beyond which output is printed without further + formatting, and without affecting any subsequent column widths. + This is used as a crude alternative to column spanning. + + """ + + 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 + + class Row: + """A set of column info, plus per-row formatting controls.""" + + 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 + + def add_row(self, cols, aligns, i_col_unlimited=None): + """ + Create a new row at the bottom. + + Args: + * cols (list of string): + Per-column content. Length must match the other rows (if any). + * aligns (list of {'left', 'right'}): + Per-column alignments. Length must match 'cols'. + * i_col_unlimited (int or None): + Column beyond which content does not affect the column widths. + ( meaning contents will print without limit ). + + """ + n_cols = len(cols) + if len(aligns) != n_cols: + msg = ( + f"Number of aligns ({len(aligns)})" + f" != number of cols ({n_cols})" + ) + raise ValueError(msg) + if self.n_columns is not None: + # For now, all rows must have same number of columns + if n_cols != self.n_columns: + msg = ( + f"Number of columns ({n_cols})" + f" != existing table.n_columns ({self.n_columns})" + ) + raise ValueError(msg) + row = self.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) + + row_line = " ".join(col_texts).rstrip() + result_lines.append(row_line) + return result_lines + + def __str__(self): + return "\n".join(self.formatted_as_strings()) + + +class CubePrinter: + """ + An object created from a + :class:`iris._representation.CubeSummary`, which provides + text printout of a :class:`iris.cube.Cube`. + + This class has no internal knowledge of :class:`iris.cube.Cube`, but only + of :class:`iris._representation.CubeSummary`. + + """ + + N_INDENT_SECTION = 4 + N_INDENT_ITEM = 4 + N_INDENT_EXTRA = 4 + + def __init__(self, cube_or_summary): + """ + An object that provides a printout of a cube. + + Args: + + * cube_or_summary (Cube or CubeSummary): + If a cube, first create a CubeSummary from it. + + + .. note:: + The CubePrinter is based on a digest of a CubeSummary, but does + not reference or store it. + + """ + # Create our internal table from the summary, to produce the printouts. + if isinstance(cube_or_summary, CubeSummary): + cube_summary = cube_or_summary + else: + cube_summary = CubeSummary(cube_or_summary) + self.table = self._ingest_summary(cube_summary) + + def _ingest_summary(self, cube_summary): + """Make a table of strings representing the cube-summary.""" + sect_indent = " " * self.N_INDENT_SECTION + item_indent = sect_indent + " " * self.N_INDENT_ITEM + item_to_extra_indent = " " * self.N_INDENT_EXTRA + extra_indent = item_indent + item_to_extra_indent + + fullheader = cube_summary.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 cube_summary.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 cube_summary.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 need different handling + 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 or "cell method" in title: + for title, value in zip(sect.names, sect.values): + add_scalar_row(title, value) + elif "scalar cell measure" in title: + # These are just strings: nothing in the 'value' column. + for name in sect.contents: + add_scalar_row(name) + 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. + + Note: 'name_padding' sets a minimum width for the name column (#0). + + """ + + # 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() + + return table + + def _oneline_string(self, name_padding): + """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=name_padding) + + # Format (with no extra spacing) --> one-line result + (oneline_result,) = table.formatted_as_strings() + return oneline_result + + def _multiline_summary(self, name_padding): + """Produce a multi-line summary string.""" + # Get a derived table with standard 'decorations' added. + table = self._decorated_table(self.table, name_padding=name_padding) + result_lines = table.formatted_as_strings() + result = "\n".join(result_lines) + return result + + def to_string(self, oneline=False, name_padding=35): + """ + Produce a printable summary. + + Args: + * oneline (bool): + If set, produce a one-line summary. + Default is False = produce full (multiline) summary. + * name_padding (int): + The minimum width for the "name" (#0) column. + + Returns: + result (string) + + """ + if oneline: + result = self._oneline_string(name_padding) + else: + result = self._multiline_summary(name_padding) + + return result + + def __str__(self): + """Printout of self, as a full multiline string.""" + return self.to_string() diff --git a/lib/iris/_representation.py b/lib/iris/_representation/cube_summary.py similarity index 96% rename from lib/iris/_representation.py rename to lib/iris/_representation/cube_summary.py index e8cb1effd5..c7d0e15e59 100644 --- a/lib/iris/_representation.py +++ b/lib/iris/_representation/cube_summary.py @@ -218,13 +218,22 @@ def __init__(self, title, attributes): class CellMethodSection(Section): def __init__(self, title, cell_methods): self.title = title - self.contents = [str(cm) for cm in cell_methods] + self.names = [] + self.values = [] + self.contents = [] + for method in cell_methods: + name = method.method + # Remove "method: " from the front of the string, leaving the value. + value = str(method)[len(name + ": ") :] + self.names.append(name) + self.values.append(value) + content = "{}: {}".format(name, value) + self.contents.append(content) 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__`. """ @@ -311,7 +320,7 @@ def add_scalar_section(section_class, title, *args): "Scalar cell measures:", scalar_cell_measures, ) - add_scalar_section(AttributeSection, "Attributes:", cube.attributes) add_scalar_section( CellMethodSection, "Cell methods:", cube.cell_methods ) + add_scalar_section(AttributeSection, "Attributes:", cube.attributes) diff --git a/lib/iris/common/resolve.py b/lib/iris/common/resolve.py index 635298e1ac..6eb79a65f9 100644 --- a/lib/iris/common/resolve.py +++ b/lib/iris/common/resolve.py @@ -100,40 +100,40 @@ class Resolve: >>> print(cube1) air_temperature / (K) (time: 240; latitude: 37; longitude: 49) - Dimension coordinates: - time x - - - latitude - x - - longitude - - x - Auxiliary coordinates: - forecast_period x - - - Scalar coordinates: - forecast_reference_time: 1859-09-01 06:00:00 - height: 1.5 m - Attributes: - Conventions: CF-1.5 - Model scenario: A1B - STASH: m01s03i236 - source: Data from Met Office Unified Model 6.05 - Cell methods: - mean: time (6 hour) + Dimension coordinates: + time x - - + latitude - x - + longitude - - x + Auxiliary coordinates: + forecast_period x - - + Scalar coordinates: + forecast_reference_time 1859-09-01 06:00:00 + height 1.5 m + Cell methods: + mean time (6 hour) + Attributes: + Conventions CF-1.5 + Model scenario A1B + STASH m01s03i236 + source Data from Met Office Unified Model 6.05 >>> print(cube2) air_temperature / (K) (longitude: 49; latitude: 37) - Dimension coordinates: - longitude x - - latitude - x - Scalar coordinates: - forecast_period: 10794 hours - forecast_reference_time: 1859-09-01 06:00:00 - height: 1.5 m - time: 1860-06-01 00:00:00, bound=(1859-12-01 00:00:00, 1860-12-01 00:00:00) - Attributes: - Conventions: CF-1.5 - Model scenario: E1 - STASH: m01s03i236 - source: Data from Met Office Unified Model 6.05 - Cell methods: - mean: time (6 hour) + Dimension coordinates: + longitude x - + latitude - x + Scalar coordinates: + forecast_period 10794 hours + forecast_reference_time 1859-09-01 06:00:00 + height 1.5 m + time 1860-06-01 00:00:00, bound=(1859-12-01 00:00:00, 1860-12-01 00:00:00) + Cell methods: + mean time (6 hour) + Attributes: + Conventions CF-1.5 + Model scenario E1 + STASH m01s03i236 + source Data from Met Office Unified Model 6.05 >>> print(data.shape) (240, 37, 49) @@ -141,21 +141,21 @@ class Resolve: >>> result = resolver.cube(data) >>> print(result) air_temperature / (K) (time: 240; latitude: 37; longitude: 49) - Dimension coordinates: - time x - - - latitude - x - - longitude - - x - Auxiliary coordinates: - forecast_period x - - - Scalar coordinates: - forecast_reference_time: 1859-09-01 06:00:00 - height: 1.5 m - Attributes: - Conventions: CF-1.5 - STASH: m01s03i236 - source: Data from Met Office Unified Model 6.05 - Cell methods: - mean: time (6 hour) + Dimension coordinates: + time x - - + latitude - x - + longitude - - x + Auxiliary coordinates: + forecast_period x - - + Scalar coordinates: + forecast_reference_time 1859-09-01 06:00:00 + height 1.5 m + Cell methods: + mean time (6 hour) + Attributes: + Conventions CF-1.5 + STASH m01s03i236 + source Data from Met Office Unified Model 6.05 Secondly, creating an *empty* ``resolver`` instance, that may be called *multiple* times with *different* :class:`~iris.cube.Cube` operands and *different* ``data``, @@ -2401,39 +2401,39 @@ def mapped(self): >>> print(cube1) air_temperature / (K) (time: 240; latitude: 37; longitude: 49) - Dimension coordinates: - time x - - - latitude - x - - longitude - - x - Auxiliary coordinates: - forecast_period x - - - Scalar coordinates: - forecast_reference_time: 1859-09-01 06:00:00 - height: 1.5 m - Attributes: - Conventions: CF-1.5 - Model scenario: A1B - STASH: m01s03i236 - source: Data from Met Office Unified Model 6.05 - Cell methods: - mean: time (6 hour) + Dimension coordinates: + time x - - + latitude - x - + longitude - - x + Auxiliary coordinates: + forecast_period x - - + Scalar coordinates: + forecast_reference_time 1859-09-01 06:00:00 + height 1.5 m + Cell methods: + mean time (6 hour) + Attributes: + Conventions CF-1.5 + Model scenario A1B + STASH m01s03i236 + source Data from Met Office Unified Model 6.05 >>> print(cube2) air_temperature / (K) (longitude: 49; latitude: 37) - Dimension coordinates: - longitude x - - latitude - x - Scalar coordinates: - forecast_period: 10794 hours - forecast_reference_time: 1859-09-01 06:00:00 - height: 1.5 m - time: 1860-06-01 00:00:00, bound=(1859-12-01 00:00:00, 1860-12-01 00:00:00) - Attributes: - Conventions: CF-1.5 - Model scenario: E1 - STASH: m01s03i236 - source: Data from Met Office Unified Model 6.05 - Cell methods: - mean: time (6 hour) + Dimension coordinates: + longitude x - + latitude - x + Scalar coordinates: + forecast_period 10794 hours + forecast_reference_time 1859-09-01 06:00:00 + height 1.5 m + time 1860-06-01 00:00:00, bound=(1859-12-01 00:00:00, 1860-12-01 00:00:00) + Cell methods: + mean time (6 hour) + Attributes: + Conventions CF-1.5 + Model scenario E1 + STASH m01s03i236 + source Data from Met Office Unified Model 6.05 >>> Resolve().mapped is None True >>> resolver = Resolve(cube1, cube2) @@ -2469,39 +2469,39 @@ def shape(self): >>> print(cube1) air_temperature / (K) (time: 240; latitude: 37; longitude: 49) - Dimension coordinates: - time x - - - latitude - x - - longitude - - x - Auxiliary coordinates: - forecast_period x - - - Scalar coordinates: - forecast_reference_time: 1859-09-01 06:00:00 - height: 1.5 m - Attributes: - Conventions: CF-1.5 - Model scenario: A1B - STASH: m01s03i236 - source: Data from Met Office Unified Model 6.05 - Cell methods: - mean: time (6 hour) + Dimension coordinates: + time x - - + latitude - x - + longitude - - x + Auxiliary coordinates: + forecast_period x - - + Scalar coordinates: + forecast_reference_time 1859-09-01 06:00:00 + height 1.5 m + Cell methods: + mean time (6 hour) + Attributes: + Conventions CF-1.5 + Model scenario A1B + STASH m01s03i236 + source Data from Met Office Unified Model 6.05 >>> print(cube2) air_temperature / (K) (longitude: 49; latitude: 37) - Dimension coordinates: - longitude x - - latitude - x - Scalar coordinates: - forecast_period: 10794 hours - forecast_reference_time: 1859-09-01 06:00:00 - height: 1.5 m - time: 1860-06-01 00:00:00, bound=(1859-12-01 00:00:00, 1860-12-01 00:00:00) - Attributes: - Conventions: CF-1.5 - Model scenario: E1 - STASH: m01s03i236 - source: Data from Met Office Unified Model 6.05 - Cell methods: - mean: time (6 hour) + Dimension coordinates: + longitude x - + latitude - x + Scalar coordinates: + forecast_period 10794 hours + forecast_reference_time 1859-09-01 06:00:00 + height 1.5 m + time 1860-06-01 00:00:00, bound=(1859-12-01 00:00:00, 1860-12-01 00:00:00) + Cell methods: + mean time (6 hour) + Attributes: + Conventions CF-1.5 + Model scenario E1 + STASH m01s03i236 + source Data from Met Office Unified Model 6.05 >>> Resolve().shape is None True >>> Resolve(cube1, cube2).shape diff --git a/lib/iris/cube.py b/lib/iris/cube.py index 7d4f1dc33d..c6aa9634ee 100644 --- a/lib/iris/cube.py +++ b/lib/iris/cube.py @@ -739,21 +739,22 @@ class Cube(CFVariableMixin): >>> cube = iris.load_cube(iris.sample_data_path('air_temp.pp')) >>> print(cube) air_temperature / (K) (latitude: 73; longitude: 96) - Dimension coordinates: - latitude x - - longitude - x - Scalar coordinates: - forecast_period: 6477 hours, bound=(-28083.0, 6477.0) hours - forecast_reference_time: 1998-03-01 03:00:00 - pressure: 1000.0 hPa - time: 1998-12-01 00:00:00, \ -bound=(1994-12-01 00:00:00, 1998-12-01 00:00:00) - Attributes: - STASH: m01s16i203 - source: Data from Met Office Unified Model - Cell methods: - mean within years: time - mean over years: time + Dimension coordinates: + latitude x - + longitude - x + Scalar coordinates: + forecast_period \ +6477 hours, bound=(-28083.0, 6477.0) hours + forecast_reference_time 1998-03-01 03:00:00 + pressure 1000.0 hPa + time \ +1998-12-01 00:00:00, bound=(1994-12-01 00:00:00, 1998-12-01 00:00:00) + Cell methods: + mean within years time + mean over years time + Attributes: + STASH m01s16i203 + source Data from Met Office Unified Model See the :doc:`user guide` for more information. @@ -2242,424 +2243,26 @@ def aux_factories(self): """Return a tuple of all the coordinate factories.""" return tuple(self._aux_factories) - def _summary_coord_extra(self, coord, indent): - # Returns the text needed to ensure this coordinate can be - # distinguished from all others with the same name. - extra = "" - similar_coords = self.coords(coord.name()) - if len(similar_coords) > 1: - similar_coords.remove(coord) - # Look for any attributes that vary. - vary = set() - for key, value in coord.attributes.items(): - for similar_coord in similar_coords: - if key not in similar_coord.attributes: - vary.add(key) - break - if not np.array_equal( - similar_coord.attributes[key], value - ): - vary.add(key) - break - keys = sorted(vary) - bits = [ - "{}={!r}".format(key, coord.attributes[key]) for key in keys - ] - if bits: - extra = indent + ", ".join(bits) - return extra - - def _summary_extra(self, coords, summary, indent): - # Where necessary, inserts extra lines into the summary to ensure - # coordinates can be distinguished. - new_summary = [] - for coord, summary in zip(coords, summary): - new_summary.append(summary) - extra = self._summary_coord_extra(coord, indent) - if extra: - new_summary.append(extra) - return new_summary - def summary(self, shorten=False, name_padding=35): """ - Unicode string summary of the Cube with name, a list of dim coord names - versus length and optionally relevant coordinate information. - - """ - # Create a set to contain the axis names for each data dimension. - dim_names = [set() for dim in range(len(self.shape))] - - # Add the dim_coord names that participate in the associated data - # dimensions. - for dim in range(len(self.shape)): - dim_coords = self.coords(contains_dimension=dim, dim_coords=True) - if dim_coords: - dim_names[dim].add(dim_coords[0].name()) - else: - dim_names[dim].add("-- ") - - # Convert axes sets to lists and sort. - dim_names = [sorted(names, key=sorted_axes) for names in dim_names] - - # Generate textual summary of the cube dimensionality. - if self.shape == (): - dimension_header = "scalar cube" - else: - dimension_header = "; ".join( - [ - ", ".join(dim_names[dim]) + ": %d" % dim_shape - for dim, dim_shape in enumerate(self.shape) - ] - ) - - nameunit = "{name} / ({units})".format( - name=self.name(), units=self.units - ) - cube_header = "{nameunit!s:{length}} ({dimension})".format( - length=name_padding, nameunit=nameunit, dimension=dimension_header - ) - summary = "" - - # Generate full cube textual summary. - if not shorten: - indent = 10 - extra_indent = " " * 13 - - # Cache the derived coords so we can rely on consistent - # object IDs. - derived_coords = self.derived_coords - # Determine the cube coordinates that are scalar (single-valued) - # AND non-dimensioned. - dim_coords = self.dim_coords - aux_coords = self.aux_coords - all_coords = dim_coords + aux_coords + derived_coords - scalar_coords = [ - coord - for coord in all_coords - if not self.coord_dims(coord) and coord.shape == (1,) - ] - # Determine the cube coordinates that are not scalar BUT - # dimensioned. - scalar_coord_ids = set(map(id, scalar_coords)) - vector_dim_coords = [ - coord - for coord in dim_coords - if id(coord) not in scalar_coord_ids - ] - vector_aux_coords = [ - coord - for coord in aux_coords - if id(coord) not in scalar_coord_ids - ] - vector_derived_coords = [ - coord - for coord in derived_coords - if id(coord) not in scalar_coord_ids - ] + String summary of the Cube with name+units, a list of dim coord names + versus length and, optionally, a summary of all other components. - # cell measures - vector_cell_measures = [ - cm for cm in self.cell_measures() if cm.shape != (1,) - ] - - # Ancillary Variables - vector_ancillary_variables = [ - av for av in self.ancillary_variables() - ] - - # Sort scalar coordinates by name. - scalar_coords.sort(key=lambda coord: coord.name()) - # Sort vector coordinates by data dimension and name. - vector_dim_coords.sort( - key=lambda coord: (self.coord_dims(coord), coord.name()) - ) - vector_aux_coords.sort( - key=lambda coord: (self.coord_dims(coord), coord.name()) - ) - vector_derived_coords.sort( - key=lambda coord: (self.coord_dims(coord), coord.name()) - ) - - # - # Generate textual summary of cube vector coordinates. - # - def vector_summary( - vector_coords, - cube_header, - max_line_offset, - cell_measures=None, - ancillary_variables=None, - ): - """ - Generates a list of suitably aligned strings containing coord - names and dimensions indicated by one or more 'x' symbols. - - .. note:: - - The function may need to update the cube header so this is - returned with the list of strings. - - """ - if cell_measures is None: - cell_measures = [] - if ancillary_variables is None: - ancillary_variables = [] - vector_summary = [] - vectors = [] - - # Identify offsets for each dimension text marker. - alignment = np.array( - [ - index - for index, value in enumerate(cube_header) - if value == ":" - ] - ) - - # Generate basic textual summary for each vector coordinate - # - WITHOUT dimension markers. - for dim_meta in ( - vector_coords + cell_measures + ancillary_variables - ): - vector_summary.append( - "%*s%s" - % (indent, " ", iris.util.clip_string(dim_meta.name())) - ) - min_alignment = min(alignment) - - # Determine whether the cube header requires realignment - # due to one or more longer vector coordinate summaries. - if max_line_offset >= min_alignment: - delta = max_line_offset - min_alignment + 5 - cube_header = "%-*s (%s)" % ( - int(name_padding + delta), - nameunit, - dimension_header, - ) - alignment += delta - - if vector_coords: - # Generate full textual summary for each vector coordinate - # - WITH dimension markers. - for index, coord in enumerate(vector_coords): - dims = self.coord_dims(coord) - - for dim in range(len(self.shape)): - width = alignment[dim] - len(vector_summary[index]) - char = "x" if dim in dims else "-" - line = "{pad:{width}}{char}".format( - pad=" ", width=width, char=char - ) - vector_summary[index] += line - vectors = vectors + vector_coords - if cell_measures: - # Generate full textual summary for each vector cell - # measure - WITH dimension markers. - for index, cell_measure in enumerate(cell_measures): - dims = self.cell_measure_dims(cell_measure) - - for dim in range(len(self.shape)): - width = alignment[dim] - len(vector_summary[index]) - char = "x" if dim in dims else "-" - line = "{pad:{width}}{char}".format( - pad=" ", width=width, char=char - ) - vector_summary[index] += line - vectors = vectors + cell_measures - if ancillary_variables: - # Generate full textual summary for each vector ancillary - # variable - WITH dimension markers. - for index, av in enumerate(ancillary_variables): - dims = self.ancillary_variable_dims(av) - - for dim in range(len(self.shape)): - width = alignment[dim] - len(vector_summary[index]) - char = "x" if dim in dims else "-" - line = "{pad:{width}}{char}".format( - pad=" ", width=width, char=char - ) - vector_summary[index] += line - vectors = vectors + ancillary_variables - # Interleave any extra lines that are needed to distinguish - # the coordinates. - vector_summary = self._summary_extra( - vectors, vector_summary, extra_indent - ) - - return vector_summary, cube_header - - # Calculate the maximum line offset. - max_line_offset = 0 - for coord in ( - list(all_coords) - + self.ancillary_variables() - + self.cell_measures() - ): - max_line_offset = max( - max_line_offset, - len( - "%*s%s" - % ( - indent, - " ", - iris.util.clip_string(str(coord.name())), - ) - ), - ) - - if vector_dim_coords: - dim_coord_summary, cube_header = vector_summary( - vector_dim_coords, cube_header, max_line_offset - ) - summary += "\n Dimension coordinates:\n" + "\n".join( - dim_coord_summary - ) - - if vector_aux_coords: - aux_coord_summary, cube_header = vector_summary( - vector_aux_coords, cube_header, max_line_offset - ) - summary += "\n Auxiliary coordinates:\n" + "\n".join( - aux_coord_summary - ) - - if vector_derived_coords: - derived_coord_summary, cube_header = vector_summary( - vector_derived_coords, cube_header, max_line_offset - ) - summary += "\n Derived coordinates:\n" + "\n".join( - derived_coord_summary - ) - - # - # Generate summary of cube cell measures attribute - # - if vector_cell_measures: - cell_measure_summary, cube_header = vector_summary( - [], - cube_header, - max_line_offset, - cell_measures=vector_cell_measures, - ) - summary += "\n Cell measures:\n" - summary += "\n".join(cell_measure_summary) - - # - # Generate summary of cube ancillary variables attribute - # - if vector_ancillary_variables: - ancillary_variable_summary, cube_header = vector_summary( - [], - cube_header, - max_line_offset, - ancillary_variables=vector_ancillary_variables, - ) - summary += "\n Ancillary variables:\n" - summary += "\n".join(ancillary_variable_summary) - - # - # Generate textual summary of cube scalar coordinates. - # - scalar_summary = [] - - if scalar_coords: - for coord in scalar_coords: - if ( - coord.units in ["1", "no_unit", "unknown"] - or coord.units.is_time_reference() - ): - unit = "" - else: - unit = " {!s}".format(coord.units) - - # Format cell depending on type of point and whether it - # has a bound. - coord_cell = coord.cell(0) - if isinstance(coord_cell.point, str): - # Indent string type coordinates - coord_cell_split = [ - iris.util.clip_string(str(item)) - for item in coord_cell.point.split("\n") - ] - line_sep = "\n{pad:{width}}".format( - pad=" ", width=indent + len(coord.name()) + 2 - ) - coord_cell_str = line_sep.join(coord_cell_split) + unit - else: - coord_cell_cpoint = coord_cell.point - coord_cell_cbound = coord_cell.bound - - coord_cell_str = "{!s}{}".format( - coord_cell_cpoint, unit - ) - if coord_cell_cbound is not None: - bound = "({})".format( - ", ".join( - str(val) for val in coord_cell_cbound - ) - ) - coord_cell_str += ", bound={}{}".format( - bound, unit - ) - - scalar_summary.append( - "{pad:{width}}{name}: {cell}".format( - pad=" ", - width=indent, - name=coord.name(), - cell=coord_cell_str, - ) - ) - - # Interleave any extra lines that are needed to distinguish - # the coordinates. - scalar_summary = self._summary_extra( - scalar_coords, scalar_summary, extra_indent - ) - - summary += "\n Scalar coordinates:\n" + "\n".join( - scalar_summary - ) - - # cell measures - scalar_cell_measures = [ - cm for cm in self.cell_measures() if cm.shape == (1,) - ] - if scalar_cell_measures: - summary += "\n Scalar cell measures:\n" - scalar_cms = [ - " {}".format(cm.name()) - for cm in scalar_cell_measures - ] - summary += "\n".join(scalar_cms) - - # - # Generate summary of cube attributes. - # - if self.attributes: - attribute_lines = [] - for name, value in sorted(self.attributes.items()): - value = iris.util.clip_string(str(value)) - line = "{pad:{width}}{name}: {value}".format( - pad=" ", width=indent, name=name, value=value - ) - attribute_lines.append(line) - summary += "\n Attributes:\n" + "\n".join(attribute_lines) - - # - # Generate summary of cube cell methods - # - if self.cell_methods: - summary += "\n Cell methods:\n" - cm_lines = [] + Kwargs: - for cm in self.cell_methods: - cm_lines.append("%*s%s" % (indent, " ", str(cm))) - summary += "\n".join(cm_lines) + * shorten (bool): + If set, produce a one-line summary of minimal width, showing only + the cube name, units and dimensions. + When not set (default), produces a full multi-line summary string. + * name_padding (int): + Control the *minimum* width of the cube name + units, + i.e. the indent of the dimension map section. - # Construct the final cube summary. - summary = cube_header + summary + """ + from iris._representation.cube_printout import CubePrinter + printer = CubePrinter(self) + summary = printer.to_string(oneline=shorten, name_padding=name_padding) return summary def __str__(self): @@ -3861,20 +3464,21 @@ def collapsed(self, coords, aggregator, **kwargs): >>> new_cube = cube.collapsed('longitude', iris.analysis.MEAN) >>> print(new_cube) surface_temperature / (K) (time: 54; latitude: 18) - Dimension coordinates: - time x - - latitude - x - Auxiliary coordinates: - forecast_reference_time x - - Scalar coordinates: - forecast_period: 0 hours - longitude: 180.0 degrees, bound=(0.0, 360.0) degrees - Attributes: - Conventions: CF-1.5 - STASH: m01s00i024 - Cell methods: - mean: month, year - mean: longitude + Dimension coordinates: + time x - + latitude - x + Auxiliary coordinates: + forecast_reference_time x - + Scalar coordinates: + forecast_period 0 hours + longitude \ +180.0 degrees, bound=(0.0, 360.0) degrees + Cell methods: + mean month, year + mean longitude + Attributes: + Conventions CF-1.5 + STASH m01s00i024 .. note:: @@ -4090,26 +3694,26 @@ def aggregated_by(self, coords, aggregator, **kwargs): >>> print(new_cube) surface_temperature / (K) \ (time: 5; latitude: 18; longitude: 432) - Dimension coordinates: - time \ - x - - - latitude \ - - x - - longitude \ - - - x - Auxiliary coordinates: - forecast_reference_time \ - x - - - year \ - x - - - Scalar coordinates: - forecast_period: 0 hours - Attributes: - Conventions: CF-1.5 - STASH: m01s00i024 - Cell methods: - mean: month, year - mean: year + Dimension coordinates: + time \ +x - - + latitude \ +- x - + longitude \ +- - x + Auxiliary coordinates: + forecast_reference_time \ +x - - + year \ +x - - + Scalar coordinates: + forecast_period 0 hours + Cell methods: + mean month, year + mean year + Attributes: + Conventions CF-1.5 + STASH m01s00i024 """ groupby_coords = [] @@ -4295,51 +3899,52 @@ def rolling_window(self, coord, aggregator, window, **kwargs): >>> print(air_press) surface_temperature / (K) \ (time: 6; latitude: 145; longitude: 192) - Dimension coordinates: - time \ - x - - - latitude \ - - x - - longitude \ - - - x - Auxiliary coordinates: - forecast_period \ - x - - - Scalar coordinates: - forecast_reference_time: 2011-07-23 00:00:00 - realization: 10 - Attributes: - STASH: m01s00i024 - source: Data from Met Office Unified Model - um_version: 7.6 - Cell methods: - mean: time (1 hour) + Dimension coordinates: + time \ +x - - + latitude \ +- x - + longitude \ +- - x + Auxiliary coordinates: + forecast_period \ +x - - + Scalar coordinates: + forecast_reference_time 2011-07-23 00:00:00 + realization 10 + Cell methods: + mean time (1 hour) + Attributes: + STASH m01s00i024 + source \ +Data from Met Office Unified Model + um_version 7.6 >>> print(air_press.rolling_window('time', iris.analysis.MEAN, 3)) surface_temperature / (K) \ (time: 4; latitude: 145; longitude: 192) - Dimension coordinates: - time \ - x - - - latitude \ - - x - - longitude \ - - - x - Auxiliary coordinates: - forecast_period \ - x - - - Scalar coordinates: - forecast_reference_time: 2011-07-23 00:00:00 - realization: 10 - Attributes: - STASH: m01s00i024 - source: Data from Met Office Unified Model - um_version: 7.6 - Cell methods: - mean: time (1 hour) - mean: time - + Dimension coordinates: + time \ +x - - + latitude \ +- x - + longitude \ +- - x + Auxiliary coordinates: + forecast_period \ +x - - + Scalar coordinates: + forecast_reference_time 2011-07-23 00:00:00 + realization 10 + Cell methods: + mean time (1 hour) + mean time + Attributes: + STASH m01s00i024 + source \ +Data from Met Office Unified Model + um_version 7.6 Notice that the forecast_period dimension now represents the 4 possible windows of size 3 from the original cube. diff --git a/lib/iris/experimental/representation.py b/lib/iris/experimental/representation.py index c33c162d4c..b9a6de6e65 100644 --- a/lib/iris/experimental/representation.py +++ b/lib/iris/experimental/representation.py @@ -93,8 +93,8 @@ def __init__(self, cube): "Ancillary variables:": None, "Scalar coordinates:": None, "Scalar cell measures:": None, - "Attributes:": None, "Cell methods:": None, + "Attributes:": None, } self.dim_desc_coords = [ "Dimension coordinates:", diff --git a/lib/iris/tests/integration/test_netcdf.py b/lib/iris/tests/integration/test_netcdf.py index 3ff5bbb19d..3d4e972c57 100644 --- a/lib/iris/tests/integration/test_netcdf.py +++ b/lib/iris/tests/integration/test_netcdf.py @@ -300,13 +300,13 @@ def test_round_trip(self): def test_print(self): cube = iris.load_cube(self.fname) printed = cube.__str__() - self.assertTrue( + self.assertIn( ( - "\n Cell measures:\n cell_area" - " - - " + "Cell measures:\n" + " cell_area - - " " x x" - ) - in printed + ), + printed, ) diff --git a/lib/iris/tests/results/cdm/TestStockCubeStringRepresentations/0d_str.txt b/lib/iris/tests/results/cdm/TestStockCubeStringRepresentations/0d_str.txt index e79f060898..a6738e654f 100644 --- a/lib/iris/tests/results/cdm/TestStockCubeStringRepresentations/0d_str.txt +++ b/lib/iris/tests/results/cdm/TestStockCubeStringRepresentations/0d_str.txt @@ -1,13 +1,13 @@ air_potential_temperature / (K) (scalar cube) - Scalar coordinates: - altitude: 418.69836 m, bound=(413.93686, 426.63434) m - forecast_period: 0.0 hours - grid_latitude: -0.1278 degrees, bound=(-0.12825, -0.12735) degrees - grid_longitude: 359.5796 degrees, bound=(359.57916, 359.58005) degrees - level_height: 5.0 m, bound=(0.0, 13.333332) m - model_level_number: 1 - sigma: 0.9994238, bound=(1.0, 0.99846387) - surface_altitude: 413.93686 m - time: 2009-09-09 17:10:00 - Attributes: - source: Iris test case \ No newline at end of file + Scalar coordinates: + altitude 418.69836 m, bound=(413.93686, 426.63434) m + forecast_period 0.0 hours + grid_latitude -0.1278 degrees, bound=(-0.12825, -0.12735) degrees + grid_longitude 359.5796 degrees, bound=(359.57916, 359.58005) degrees + level_height 5.0 m, bound=(0.0, 13.333332) m + model_level_number 1 + sigma 0.9994238, bound=(1.0, 0.99846387) + surface_altitude 413.93686 m + time 2009-09-09 17:10:00 + Attributes: + source Iris test case \ No newline at end of file diff --git a/lib/iris/tests/results/cdm/TestStockCubeStringRepresentations/1d_str.txt b/lib/iris/tests/results/cdm/TestStockCubeStringRepresentations/1d_str.txt index 204df1af1c..95f7e7b57e 100644 --- a/lib/iris/tests/results/cdm/TestStockCubeStringRepresentations/1d_str.txt +++ b/lib/iris/tests/results/cdm/TestStockCubeStringRepresentations/1d_str.txt @@ -1,16 +1,16 @@ air_potential_temperature / (K) (grid_longitude: 100) - Dimension coordinates: - grid_longitude x - Auxiliary coordinates: - surface_altitude x - Derived coordinates: - altitude x - Scalar coordinates: - forecast_period: 0.0 hours - grid_latitude: -0.1278 degrees, bound=(-0.12825, -0.12735) degrees - level_height: 5.0 m, bound=(0.0, 13.333332) m - model_level_number: 1 - sigma: 0.9994238, bound=(1.0, 0.99846387) - time: 2009-09-09 17:10:00 - Attributes: - source: Iris test case \ No newline at end of file + Dimension coordinates: + grid_longitude x + Auxiliary coordinates: + surface_altitude x + Derived coordinates: + altitude x + Scalar coordinates: + forecast_period 0.0 hours + grid_latitude -0.1278 degrees, bound=(-0.12825, -0.12735) degrees + level_height 5.0 m, bound=(0.0, 13.333332) m + model_level_number 1 + sigma 0.9994238, bound=(1.0, 0.99846387) + time 2009-09-09 17:10:00 + Attributes: + source Iris test case \ No newline at end of file diff --git a/lib/iris/tests/results/cdm/TestStockCubeStringRepresentations/2d_str.txt b/lib/iris/tests/results/cdm/TestStockCubeStringRepresentations/2d_str.txt index a440a28e04..c4184d199a 100644 --- a/lib/iris/tests/results/cdm/TestStockCubeStringRepresentations/2d_str.txt +++ b/lib/iris/tests/results/cdm/TestStockCubeStringRepresentations/2d_str.txt @@ -1,16 +1,16 @@ air_potential_temperature / (K) (grid_latitude: 100; grid_longitude: 100) - Dimension coordinates: - grid_latitude x - - grid_longitude - x - Auxiliary coordinates: - surface_altitude x x - Derived coordinates: - altitude x x - Scalar coordinates: - forecast_period: 0.0 hours - level_height: 5.0 m, bound=(0.0, 13.333332) m - model_level_number: 1 - sigma: 0.9994238, bound=(1.0, 0.99846387) - time: 2009-09-09 17:10:00 - Attributes: - source: Iris test case \ No newline at end of file + Dimension coordinates: + grid_latitude x - + grid_longitude - x + Auxiliary coordinates: + surface_altitude x x + Derived coordinates: + altitude x x + Scalar coordinates: + forecast_period 0.0 hours + level_height 5.0 m, bound=(0.0, 13.333332) m + model_level_number 1 + sigma 0.9994238, bound=(1.0, 0.99846387) + time 2009-09-09 17:10:00 + Attributes: + source Iris test case \ No newline at end of file diff --git a/lib/iris/tests/results/cdm/TestStockCubeStringRepresentations/3d_str.txt b/lib/iris/tests/results/cdm/TestStockCubeStringRepresentations/3d_str.txt index e171d0c513..af81d4e991 100644 --- a/lib/iris/tests/results/cdm/TestStockCubeStringRepresentations/3d_str.txt +++ b/lib/iris/tests/results/cdm/TestStockCubeStringRepresentations/3d_str.txt @@ -1,16 +1,16 @@ air_potential_temperature / (K) (model_level_number: 70; grid_latitude: 100; grid_longitude: 100) - Dimension coordinates: - model_level_number x - - - grid_latitude - x - - grid_longitude - - x - Auxiliary coordinates: - level_height x - - - sigma x - - - surface_altitude - x x - Derived coordinates: - altitude x x x - Scalar coordinates: - forecast_period: 0.0 hours - time: 2009-09-09 17:10:00 - Attributes: - source: Iris test case \ No newline at end of file + Dimension coordinates: + model_level_number x - - + grid_latitude - x - + grid_longitude - - x + Auxiliary coordinates: + level_height x - - + sigma x - - + surface_altitude - x x + Derived coordinates: + altitude x x x + Scalar coordinates: + forecast_period 0.0 hours + time 2009-09-09 17:10:00 + Attributes: + source Iris test case \ No newline at end of file diff --git a/lib/iris/tests/results/cdm/TestStockCubeStringRepresentations/4d_str.txt b/lib/iris/tests/results/cdm/TestStockCubeStringRepresentations/4d_str.txt index 053e9473f0..afcdedf100 100644 --- a/lib/iris/tests/results/cdm/TestStockCubeStringRepresentations/4d_str.txt +++ b/lib/iris/tests/results/cdm/TestStockCubeStringRepresentations/4d_str.txt @@ -1,16 +1,16 @@ air_potential_temperature / (K) (time: 6; model_level_number: 70; grid_latitude: 100; grid_longitude: 100) - Dimension coordinates: - time x - - - - model_level_number - x - - - grid_latitude - - x - - grid_longitude - - - x - Auxiliary coordinates: - level_height - x - - - sigma - x - - - surface_altitude - - x x - Derived coordinates: - altitude - x x x - Scalar coordinates: - forecast_period: 0.0 hours - Attributes: - source: Iris test case \ No newline at end of file + Dimension coordinates: + time x - - - + model_level_number - x - - + grid_latitude - - x - + grid_longitude - - - x + Auxiliary coordinates: + level_height - x - - + sigma - x - - + surface_altitude - - x x + Derived coordinates: + altitude - x x x + Scalar coordinates: + forecast_period 0.0 hours + Attributes: + source Iris test case \ No newline at end of file diff --git a/lib/iris/tests/results/cdm/str_repr/0d_cube.__str__.txt b/lib/iris/tests/results/cdm/str_repr/0d_cube.__str__.txt index afe329f38d..6a3276d861 100644 --- a/lib/iris/tests/results/cdm/str_repr/0d_cube.__str__.txt +++ b/lib/iris/tests/results/cdm/str_repr/0d_cube.__str__.txt @@ -1,11 +1,11 @@ air_temperature / (K) (scalar cube) - Scalar coordinates: - forecast_period: 6477.0 hours - forecast_reference_time: 1998-03-06 03:00:00 - latitude: 89.999985 degrees - longitude: 0.0 degrees - pressure: 1000.0 hPa - time: 1998-12-01 00:00:00 - Attributes: - STASH: m01s16i203 - source: Data from Met Office Unified Model \ No newline at end of file + Scalar coordinates: + forecast_period 6477.0 hours + forecast_reference_time 1998-03-06 03:00:00 + latitude 89.999985 degrees + longitude 0.0 degrees + pressure 1000.0 hPa + time 1998-12-01 00:00:00 + Attributes: + STASH m01s16i203 + source Data from Met Office Unified Model \ No newline at end of file diff --git a/lib/iris/tests/results/cdm/str_repr/0d_cube.__unicode__.txt b/lib/iris/tests/results/cdm/str_repr/0d_cube.__unicode__.txt index afe329f38d..6a3276d861 100644 --- a/lib/iris/tests/results/cdm/str_repr/0d_cube.__unicode__.txt +++ b/lib/iris/tests/results/cdm/str_repr/0d_cube.__unicode__.txt @@ -1,11 +1,11 @@ air_temperature / (K) (scalar cube) - Scalar coordinates: - forecast_period: 6477.0 hours - forecast_reference_time: 1998-03-06 03:00:00 - latitude: 89.999985 degrees - longitude: 0.0 degrees - pressure: 1000.0 hPa - time: 1998-12-01 00:00:00 - Attributes: - STASH: m01s16i203 - source: Data from Met Office Unified Model \ No newline at end of file + Scalar coordinates: + forecast_period 6477.0 hours + forecast_reference_time 1998-03-06 03:00:00 + latitude 89.999985 degrees + longitude 0.0 degrees + pressure 1000.0 hPa + time 1998-12-01 00:00:00 + Attributes: + STASH m01s16i203 + source Data from Met Office Unified Model \ No newline at end of file diff --git a/lib/iris/tests/results/cdm/str_repr/cell_methods.__str__.txt b/lib/iris/tests/results/cdm/str_repr/cell_methods.__str__.txt index d932b03a35..ba93542e51 100644 --- a/lib/iris/tests/results/cdm/str_repr/cell_methods.__str__.txt +++ b/lib/iris/tests/results/cdm/str_repr/cell_methods.__str__.txt @@ -1,17 +1,17 @@ air_temperature / (K) (latitude: 73; longitude: 96) - Dimension coordinates: - latitude x - - longitude - x - Scalar coordinates: - forecast_period: 6477.0 hours - forecast_reference_time: 1998-03-06 03:00:00 - pressure: 1000.0 hPa - time: 1998-12-01 00:00:00 - Attributes: - STASH: m01s16i203 - source: Data from Met Office Unified Model - Cell methods: - mean: longitude (6 minutes, This is a test comment), latitude (12 minutes) - average: longitude (6 minutes, This is another test comment), latitude (15 minutes, This is another comment) - average: longitude, latitude - percentile: longitude (6 minutes, This is another test comment) \ No newline at end of file + Dimension coordinates: + latitude x - + longitude - x + Scalar coordinates: + forecast_period 6477.0 hours + forecast_reference_time 1998-03-06 03:00:00 + pressure 1000.0 hPa + time 1998-12-01 00:00:00 + Cell methods: + mean longitude (6 minutes, This is a test comment), latitude (12 minutes) + average longitude (6 minutes, This is another test comment), latitude (15 minutes, This is another comment) + average longitude, latitude + percentile longitude (6 minutes, This is another test comment) + Attributes: + STASH m01s16i203 + source Data from Met Office Unified Model \ No newline at end of file diff --git a/lib/iris/tests/results/cdm/str_repr/missing_coords_cube.str.txt b/lib/iris/tests/results/cdm/str_repr/missing_coords_cube.str.txt index 84af287efa..1b86bd6597 100644 --- a/lib/iris/tests/results/cdm/str_repr/missing_coords_cube.str.txt +++ b/lib/iris/tests/results/cdm/str_repr/missing_coords_cube.str.txt @@ -1,14 +1,14 @@ air_potential_temperature / (K) (-- : 6; -- : 70; grid_latitude: 100; grid_longitude: 100) - Dimension coordinates: - grid_latitude - - x - - grid_longitude - - - x - Auxiliary coordinates: - level_height - x - - - sigma - x - - - surface_altitude - - x x - Derived coordinates: - altitude - x x x - Scalar coordinates: - forecast_period: 0.0 hours - Attributes: - source: Iris test case \ No newline at end of file + Dimension coordinates: + grid_latitude - - x - + grid_longitude - - - x + Auxiliary coordinates: + level_height - x - - + sigma - x - - + surface_altitude - - x x + Derived coordinates: + altitude - x x x + Scalar coordinates: + forecast_period 0.0 hours + Attributes: + source Iris test case \ No newline at end of file diff --git a/lib/iris/tests/results/cdm/str_repr/multi_dim_coord.__str__.txt b/lib/iris/tests/results/cdm/str_repr/multi_dim_coord.__str__.txt index 17a75f7b8b..8fd35aa78d 100644 --- a/lib/iris/tests/results/cdm/str_repr/multi_dim_coord.__str__.txt +++ b/lib/iris/tests/results/cdm/str_repr/multi_dim_coord.__str__.txt @@ -1,9 +1,9 @@ test 2d dimensional cube / (meters) (dim1: 5; dim2: 10) - Dimension coordinates: - dim1 x - - dim2 - x - Auxiliary coordinates: - my_multi_dim_coord x x - Scalar coordinates: - air_temperature: 23.3 K - an_other: 3.0 meters \ No newline at end of file + Dimension coordinates: + dim1 x - + dim2 - x + Auxiliary coordinates: + my_multi_dim_coord x x + Scalar coordinates: + air_temperature 23.3 K + an_other 3.0 meters \ No newline at end of file diff --git a/lib/iris/tests/results/cdm/str_repr/similar.__str__.txt b/lib/iris/tests/results/cdm/str_repr/similar.__str__.txt index 45be83a424..fc274ed4c1 100644 --- a/lib/iris/tests/results/cdm/str_repr/similar.__str__.txt +++ b/lib/iris/tests/results/cdm/str_repr/similar.__str__.txt @@ -1,18 +1,18 @@ -air_temperature / (K) (latitude: 73; longitude: 96) - Dimension coordinates: - latitude x - - longitude - x - sensor_id=808, status=2 - Auxiliary coordinates: - latitude x - - test='True' - longitude - x - ref='A8T-22', sensor_id=810 - Scalar coordinates: - forecast_period: 6477.0 hours - forecast_reference_time: 1998-03-06 03:00:00 - pressure: 1000.0 hPa - time: 1998-12-01 00:00:00 - Attributes: - STASH: m01s16i203 - source: Data from Met Office Unified Model \ No newline at end of file +air_temperature / (K) (latitude: 73; longitude: 96) + Dimension coordinates: + latitude x - + longitude - x + sensor_id=808, status=2 + Auxiliary coordinates: + latitude x - + test='True' + longitude - x + ref='A8T-22', sensor_id=810 + Scalar coordinates: + forecast_period 6477.0 hours + forecast_reference_time 1998-03-06 03:00:00 + pressure 1000.0 hPa + time 1998-12-01 00:00:00 + Attributes: + STASH m01s16i203 + source Data from Met Office Unified Model \ No newline at end of file diff --git a/lib/iris/tests/results/cdm/str_repr/simple.__str__.txt b/lib/iris/tests/results/cdm/str_repr/simple.__str__.txt index 59e3aba236..f1a6d5bae2 100644 --- a/lib/iris/tests/results/cdm/str_repr/simple.__str__.txt +++ b/lib/iris/tests/results/cdm/str_repr/simple.__str__.txt @@ -1,6 +1,6 @@ -thingness / (1) (foo: 11) - Dimension coordinates: - foo x - Auxiliary coordinates: - This is a really, really, really, really long long_name that must be clipped... x - This is a short long_name x \ No newline at end of file +thingness / (1) (foo: 11) + Dimension coordinates: + foo x + Auxiliary coordinates: + This is a really, really, really, really long long_name that must be clipped... x + This is a short long_name x \ No newline at end of file diff --git a/lib/iris/tests/results/cdm/str_repr/unicode_attribute.__str__.ascii.txt b/lib/iris/tests/results/cdm/str_repr/unicode_attribute.__str__.ascii.txt deleted file mode 100644 index e72c28732d..0000000000 --- a/lib/iris/tests/results/cdm/str_repr/unicode_attribute.__str__.ascii.txt +++ /dev/null @@ -1,5 +0,0 @@ -thingness / (1) (foo: 11) - Dimension coordinates: - foo x - Attributes: - source: ?abcd? \ No newline at end of file diff --git a/lib/iris/tests/results/cdm/str_repr/unicode_attribute.__str__.utf8.txt b/lib/iris/tests/results/cdm/str_repr/unicode_attribute.__str__.utf8.txt deleted file mode 100644 index 12b5a21ef9..0000000000 --- a/lib/iris/tests/results/cdm/str_repr/unicode_attribute.__str__.utf8.txt +++ /dev/null @@ -1,5 +0,0 @@ -thingness / (1) (foo: 11) - Dimension coordinates: - foo x - Attributes: - source: ꀀabcd޴ \ No newline at end of file diff --git a/lib/iris/tests/results/cdm/str_repr/unicode_attribute.__unicode__.txt b/lib/iris/tests/results/cdm/str_repr/unicode_attribute.__unicode__.txt index 12b5a21ef9..29c181345c 100644 --- a/lib/iris/tests/results/cdm/str_repr/unicode_attribute.__unicode__.txt +++ b/lib/iris/tests/results/cdm/str_repr/unicode_attribute.__unicode__.txt @@ -1,5 +1,5 @@ thingness / (1) (foo: 11) - Dimension coordinates: - foo x - Attributes: - source: ꀀabcd޴ \ No newline at end of file + Dimension coordinates: + foo x + Attributes: + source ꀀabcd޴ \ No newline at end of file diff --git a/lib/iris/tests/results/derived/no_orog.__str__.txt b/lib/iris/tests/results/derived/no_orog.__str__.txt index 1433586a11..e277b5d276 100644 --- a/lib/iris/tests/results/derived/no_orog.__str__.txt +++ b/lib/iris/tests/results/derived/no_orog.__str__.txt @@ -1,16 +1,16 @@ air_potential_temperature / (K) (time: 6; model_level_number: 70; grid_latitude: 100; grid_longitude: 100) - Dimension coordinates: - time x - - - - model_level_number - x - - - grid_latitude - - x - - grid_longitude - - - x - Auxiliary coordinates: - level_height - x - - - sigma - x - - - surface_altitude - - x x - Derived coordinates: - altitude - x - - - Scalar coordinates: - forecast_period: 0.0 hours - Attributes: - source: Iris test case \ No newline at end of file + Dimension coordinates: + time x - - - + model_level_number - x - - + grid_latitude - - x - + grid_longitude - - - x + Auxiliary coordinates: + level_height - x - - + sigma - x - - + surface_altitude - - x x + Derived coordinates: + altitude - x - - + Scalar coordinates: + forecast_period 0.0 hours + Attributes: + source Iris test case \ No newline at end of file diff --git a/lib/iris/tests/results/derived/removed_orog.__str__.txt b/lib/iris/tests/results/derived/removed_orog.__str__.txt index 22ec52213d..0c24cded80 100644 --- a/lib/iris/tests/results/derived/removed_orog.__str__.txt +++ b/lib/iris/tests/results/derived/removed_orog.__str__.txt @@ -1,15 +1,15 @@ air_potential_temperature / (K) (time: 6; model_level_number: 70; grid_latitude: 100; grid_longitude: 100) - Dimension coordinates: - time x - - - - model_level_number - x - - - grid_latitude - - x - - grid_longitude - - - x - Auxiliary coordinates: - level_height - x - - - sigma - x - - - Derived coordinates: - altitude - x - - - Scalar coordinates: - forecast_period: 0.0 hours - Attributes: - source: Iris test case \ No newline at end of file + Dimension coordinates: + time x - - - + model_level_number - x - - + grid_latitude - - x - + grid_longitude - - - x + Auxiliary coordinates: + level_height - x - - + sigma - x - - + Derived coordinates: + altitude - x - - + Scalar coordinates: + forecast_period 0.0 hours + Attributes: + source Iris test case \ No newline at end of file diff --git a/lib/iris/tests/results/derived/removed_sigma.__str__.txt b/lib/iris/tests/results/derived/removed_sigma.__str__.txt index ffba49bf8d..94e850ec62 100644 --- a/lib/iris/tests/results/derived/removed_sigma.__str__.txt +++ b/lib/iris/tests/results/derived/removed_sigma.__str__.txt @@ -1,15 +1,15 @@ air_potential_temperature / (K) (time: 6; model_level_number: 70; grid_latitude: 100; grid_longitude: 100) - Dimension coordinates: - time x - - - - model_level_number - x - - - grid_latitude - - x - - grid_longitude - - - x - Auxiliary coordinates: - level_height - x - - - surface_altitude - - x x - Derived coordinates: - altitude - x x x - Scalar coordinates: - forecast_period: 0.0 hours - Attributes: - source: Iris test case \ No newline at end of file + Dimension coordinates: + time x - - - + model_level_number - x - - + grid_latitude - - x - + grid_longitude - - - x + Auxiliary coordinates: + level_height - x - - + surface_altitude - - x x + Derived coordinates: + altitude - x x x + Scalar coordinates: + forecast_period 0.0 hours + Attributes: + source Iris test case \ No newline at end of file diff --git a/lib/iris/tests/unit/cube/test_Cube.py b/lib/iris/tests/unit/cube/test_Cube.py index e6e973c58a..a9becb6dc4 100644 --- a/lib/iris/tests/unit/cube/test_Cube.py +++ b/lib/iris/tests/unit/cube/test_Cube.py @@ -579,10 +579,10 @@ def test_ancillary_variable(self): cube.add_ancillary_variable(av, 0) expected_summary = ( "unknown / (unknown) (-- : 2; -- : 3)\n" - " Ancillary variables:\n" - " status_flag x -" + " Ancillary variables:\n" + " status_flag x -" ) - self.assertEqual(cube.summary(), expected_summary) + self.assertEqual(expected_summary, cube.summary()) def test_similar_coords(self): coord1 = AuxCoord( diff --git a/lib/iris/tests/unit/experimental/representation/test_CubeRepresentation.py b/lib/iris/tests/unit/experimental/representation/test_CubeRepresentation.py index 008acf954b..b05e19e1ee 100644 --- a/lib/iris/tests/unit/experimental/representation/test_CubeRepresentation.py +++ b/lib/iris/tests/unit/experimental/representation/test_CubeRepresentation.py @@ -197,8 +197,11 @@ def test_headings__attributes(self): 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) + for method in self.cube.cell_methods: + name = method.method + value = str(method)[len(name + ": ") :] + self.assertIn(name, content_str) + self.assertIn(value, content_str) @tests.skip_data @@ -354,14 +357,6 @@ def test_not_included(self): for heading in not_included: self.assertNotIn(heading, self.result) - def test_handle_newline(self): - cube = self.cube - cube.attributes["lines"] = "first\nsecond" - representer = CubeRepresentation(cube) - representer._get_bits(representer._get_lines()) - result = representer._make_content() - self.assertIn("first
second", result) - @tests.skip_data class Test_repr_html(tests.IrisTest): diff --git a/lib/iris/tests/unit/representation/cube_printout/__init__.py b/lib/iris/tests/unit/representation/cube_printout/__init__.py new file mode 100644 index 0000000000..50ab3f8e45 --- /dev/null +++ b/lib/iris/tests/unit/representation/cube_printout/__init__.py @@ -0,0 +1,6 @@ +# 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. +"""Unit tests for the :mod:`iris._representation.cube_printout` module.""" diff --git a/lib/iris/tests/unit/representation/cube_printout/test_CubePrintout.py b/lib/iris/tests/unit/representation/cube_printout/test_CubePrintout.py new file mode 100644 index 0000000000..f49c9f9c0c --- /dev/null +++ b/lib/iris/tests/unit/representation/cube_printout/test_CubePrintout.py @@ -0,0 +1,518 @@ +# 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. +"""Unit tests for :class:`iris._representation.cube_printout.CubePrintout`.""" +import iris.tests as tests # isort:skip + +import numpy as np + +from iris._representation.cube_printout import CubePrinter +from iris._representation.cube_summary import CubeSummary +from iris.coords import ( + AncillaryVariable, + AuxCoord, + CellMeasure, + CellMethod, + DimCoord, +) +from iris.cube import Cube + + +class TestCubePrintout___str__(tests.IrisTest): + def test_str(self): + # Just check that its str representation is the 'to_string' result. + cube = Cube(0) + printer = CubePrinter(CubeSummary(cube)) + result = str(printer) + self.assertEqual(result, printer.to_string()) + + +def cube_replines(cube, **kwargs): + return CubePrinter(cube).to_string(**kwargs).split("\n") + + +class TestCubePrintout__to_string(tests.IrisTest): + def test_empty(self): + cube = Cube([0]) + rep = cube_replines(cube) + expect = ["unknown / (unknown) (-- : 1)"] + self.assertEqual(expect, rep) + + def test_shortform__default(self): + cube = Cube([0]) + expect = ["unknown / (unknown) (-- : 1)"] + # In this case, default one-line is the same. + rep = cube_replines(cube, oneline=True) + self.assertEqual(expect, rep) + + def test_shortform__compressed(self): + cube = Cube([0]) + rep = cube_replines(cube, oneline=True, name_padding=0) + expect = ["unknown / (unknown) (-- : 1)"] + self.assertEqual(rep, expect) + + def _sample_wide_cube(self): + cube = Cube([0, 1]) + cube.add_aux_coord( + AuxCoord( + [0, 1], + long_name="long long long long long long long long name", + ), + 0, + ) + return cube + + def test_wide_cube(self): + # For comparison with the shortform and padding-controlled cases. + cube = self._sample_wide_cube() + rep = cube_replines(cube) + expect_full = [ + "unknown / (unknown) (-- : 2)", + " Auxiliary coordinates:", + " long long long long long long long long name x", + ] + self.assertEqual(expect_full, rep) + + def test_shortform__wide__default(self): + cube = self._sample_wide_cube() + rep = cube_replines(cube, oneline=True) + # *default* one-line is shorter than full header, but not minimal. + expect = ["unknown / (unknown) (-- : 2)"] + self.assertEqual(rep, expect) + + def test_shortform__wide__compressed(self): + cube = self._sample_wide_cube() + rep = cube_replines(cube, oneline=True, name_padding=0) + expect = ["unknown / (unknown) (-- : 2)"] + self.assertEqual(rep, expect) + + def test_shortform__wide__intermediate(self): + cube = self._sample_wide_cube() + rep = cube_replines(cube, oneline=True, name_padding=25) + expect = ["unknown / (unknown) (-- : 2)"] + self.assertEqual(expect, rep) + + def test_scalar_cube_summaries(self): + cube = Cube(0) + expect = ["unknown / (unknown) (scalar cube)"] + rep = cube_replines(cube) + self.assertEqual(expect, rep) + # Shortform is the same. + rep = cube_replines(cube, oneline=True) + self.assertEqual(expect, rep) + + def test_name_padding(self): + cube = Cube([1, 2], long_name="cube_accel", units="ms-2") + rep = cube_replines(cube) + self.assertEqual(rep, ["cube_accel / (ms-2) (-- : 2)"]) + rep = cube_replines(cube, name_padding=0) + self.assertEqual(rep, ["cube_accel / (ms-2) (-- : 2)"]) + rep = cube_replines(cube, name_padding=25) + self.assertEqual(rep, ["cube_accel / (ms-2) (-- : 2)"]) + + def test_columns_long_coordname(self): + cube = Cube([0], long_name="short", units=1) + coord = AuxCoord( + [0], long_name="very_very_very_very_very_long_coord_name" + ) + cube.add_aux_coord(coord, 0) + rep = cube_replines(cube) + expected = [ + "short / (1) (-- : 1)", + " Auxiliary coordinates:", + " very_very_very_very_very_long_coord_name x", + ] + self.assertEqual(expected, rep) + rep = cube_replines(cube, oneline=True) + # Note: the default short-form is short-ER, but not minimal. + short_expected = ["short / (1) (-- : 1)"] + self.assertEqual(short_expected, rep) + + def test_columns_long_attribute(self): + cube = Cube([0], long_name="short", units=1) + cube.attributes[ + "very_very_very_very_very_long_name" + ] = "longish string extends beyond dim columns" + rep = cube_replines(cube) + expected = [ + "short / (1) (-- : 1)", + " Attributes:", + ( + " very_very_very_very_very_long_name " + "longish string extends beyond dim columns" + ), + ] + self.assertEqual(rep, expected) + + def test_coord_distinguishing_attributes(self): + # Printout of differing attributes to differentiate same-named coords. + # include : vector + scalar + cube = Cube([0, 1], long_name="name", units=1) + # Add a pair of vector coords with same name but different attributes. + cube.add_aux_coord( + AuxCoord([0, 1], long_name="co1", attributes=dict(a=1)), 0 + ) + cube.add_aux_coord( + AuxCoord([0, 1], long_name="co1", attributes=dict(a=2)), 0 + ) + # Likewise for scalar coords with same name but different attributes. + cube.add_aux_coord( + AuxCoord([0], long_name="co2", attributes=dict(a=10, b=12)) + ) + cube.add_aux_coord( + AuxCoord([1], long_name="co2", attributes=dict(a=10, b=11)) + ) + + rep = cube_replines(cube) + expected = [ + "name / (1) (-- : 2)", + " Auxiliary coordinates:", + " co1 x", + " a=1", + " co1 x", + " a=2", + " Scalar coordinates:", + " co2 0", + " b=12", + " co2 1", + " b=11", + ] + self.assertEqual(rep, expected) + + def test_coord_extra_attributes__array(self): + cube = Cube(0, long_name="name", units=1) + # Add a pair of vector coords with same name but different attributes. + array1 = np.arange(0, 3) + array2 = np.arange(10, 13) + cube.add_aux_coord( + AuxCoord([1.2], long_name="co1", attributes=dict(a=1, arr=array1)) + ) + cube.add_aux_coord( + AuxCoord([3.4], long_name="co1", attributes=dict(a=1, arr=array2)) + ) + + rep = cube_replines(cube) + expected = [ + "name / (1) (scalar cube)", + " Scalar coordinates:", + " co1 1.2", + " arr=array([0, 1, 2])", + " co1 3.4", + " arr=array([10, 11, 12])", + ] + self.assertEqual(rep, expected) + + def test_coord_extra_attributes__array__long(self): + # Also test with a long array representation. + # NOTE: this also pushes the dimension map right-wards. + array = 10 + np.arange(24.0).reshape((2, 3, 4)) + cube = Cube(0, long_name="name", units=1) + cube.add_aux_coord(AuxCoord([1], long_name="co")) + cube.add_aux_coord( + AuxCoord([2], long_name="co", attributes=dict(a=array + 1.0)) + ) + + rep = cube_replines(cube) + expected = [ + ( + "name / (1) " + " (scalar cube)" + ), + " Scalar coordinates:", + ( + " co " + " 1" + ), + ( + " co " + " 2" + ), + ( + " a=array([[[11., 12., 13., 14.], [15., 16., 17.," + " 18.], [19., 20., 21., 22.]],..." + ), + ] + self.assertEqual(rep, expected) + + def test_coord_extra_attributes__string(self): + cube = Cube(0, long_name="name", units=1) + cube.add_aux_coord(AuxCoord([1], long_name="co")) + cube.add_aux_coord( + AuxCoord( + [2], long_name="co", attributes=dict(note="string content") + ) + ) + rep = cube_replines(cube) + expected = [ + "name / (1) (scalar cube)", + " Scalar coordinates:", + " co 1", + " co 2", + " note='string content'", + ] + self.assertEqual(rep, expected) + + def test_coord_extra_attributes__string_escaped(self): + cube = Cube(0, long_name="name", units=1) + cube.add_aux_coord(AuxCoord([1], long_name="co")) + cube.add_aux_coord( + AuxCoord( + [2], + long_name="co", + attributes=dict(note="line 1\nline 2\tends."), + ) + ) + rep = cube_replines(cube) + expected = [ + "name / (1) (scalar cube)", + " Scalar coordinates:", + " co 1", + " co 2", + " note='line 1\\nline 2\\tends.'", + ] + self.assertEqual(rep, expected) + + def test_coord_extra_attributes__string_overlong(self): + cube = Cube(0, long_name="name", units=1) + cube.add_aux_coord(AuxCoord([1], long_name="co")) + long_string = ( + "this is very very very very very very very " + "very very very very very very very long." + ) + cube.add_aux_coord( + AuxCoord([2], long_name="co", attributes=dict(note=long_string)) + ) + rep = cube_replines(cube) + expected = [ + ( + "name / (1) " + " (scalar cube)" + ), + " Scalar coordinates:", + ( + " co " + " 1" + ), + ( + " co " + " 2" + ), + ( + " note='this is very very very very " + "very very very very very very very very..." + ), + ] + self.assertEqual(rep, expected) + + def test_section_vector_dimcoords(self): + cube = Cube(np.zeros((2, 3)), long_name="name", units=1) + cube.add_dim_coord(DimCoord([0, 1], long_name="y"), 0) + cube.add_dim_coord(DimCoord([0, 1, 2], long_name="x"), 1) + + rep = cube_replines(cube) + expected = [ + "name / (1) (y: 2; x: 3)", + " Dimension coordinates:", + " y x -", + " x - x", + ] + self.assertEqual(rep, expected) + + def test_section_vector_auxcoords(self): + cube = Cube(np.zeros((2, 3)), long_name="name", units=1) + cube.add_aux_coord(DimCoord([0, 1], long_name="y"), 0) + cube.add_aux_coord(DimCoord([0, 1, 2], long_name="x"), 1) + + rep = cube_replines(cube) + expected = [ + "name / (1) (-- : 2; -- : 3)", + " Auxiliary coordinates:", + " y x -", + " x - x", + ] + self.assertEqual(rep, expected) + + def test_section_vector_ancils(self): + cube = Cube(np.zeros((2, 3)), long_name="name", units=1) + cube.add_ancillary_variable( + AncillaryVariable([0, 1], long_name="av1"), 0 + ) + + rep = cube_replines(cube) + expected = [ + "name / (1) (-- : 2; -- : 3)", + " Ancillary variables:", + " av1 x -", + ] + self.assertEqual(rep, expected) + + def test_section_vector_cell_measures(self): + cube = Cube(np.zeros((2, 3)), long_name="name", units=1) + cube.add_cell_measure(CellMeasure([0, 1, 2], long_name="cm"), 1) + + rep = cube_replines(cube) + expected = [ + "name / (1) (-- : 2; -- : 3)", + " Cell measures:", + " cm - x", + ] + self.assertEqual(rep, expected) + + def test_section_scalar_coords(self): + # incl points + bounds + # TODO: ought to incorporate coord-based summary + # - which would allow for special printout of time values + cube = Cube([0], long_name="name", units=1) + cube.add_aux_coord(DimCoord([0.0], long_name="unbounded")) + cube.add_aux_coord(DimCoord([0], bounds=[[0, 7]], long_name="bounded")) + + rep = cube_replines(cube) + expected = [ + "name / (1) (-- : 1)", + " Scalar coordinates:", + " bounded 0, bound=(0, 7)", + " unbounded 0.0", + ] + self.assertEqual(rep, expected) + + def test_section_scalar_coords__string(self): + # incl a newline-escaped one + # incl a long (clipped) one + # CHECK THAT CLIPPED+ESCAPED WORKS (don't lose final quote) + cube = Cube([0], long_name="name", units=1) + cube.add_aux_coord(AuxCoord(["string-value"], long_name="text")) + long_string = ( + "A string value which is very very very very very very " + "very very very very very very very very long." + ) + cube.add_aux_coord( + AuxCoord([long_string], long_name="very_long_string") + ) + + rep = cube_replines(cube) + expected = [ + "name / (1) (-- : 1)", + " Scalar coordinates:", + " text string-value", + ( + " very_long_string A string value which is " + "very very very very very very very very very very..." + ), + ] + self.assertEqual(rep, expected) + + def test_section_scalar_cell_measures(self): + cube = Cube(np.zeros((2, 3)), long_name="name", units=1) + cube.add_cell_measure(CellMeasure([0], long_name="cm")) + + rep = cube_replines(cube) + expected = [ + "name / (1) (-- : 2; -- : 3)", + " Scalar cell measures:", + " cm", + ] + self.assertEqual(rep, expected) + + def test_section_scalar_ancillaries(self): + # There *is* no section for this. But there probably ought to be. + cube = Cube(np.zeros((2, 3)), long_name="name", units=1) + cube.add_ancillary_variable(AncillaryVariable([0], long_name="av")) + + rep = cube_replines(cube) + expected = [ + "name / (1) (-- : 2; -- : 3)", + " Ancillary variables:", + " av - -", + ] + self.assertEqual(rep, expected) + + def test_section_cube_attributes(self): + cube = Cube([0], long_name="name", units=1) + cube.attributes["number"] = 1.2 + cube.attributes["list"] = [3] + cube.attributes["string"] = "four five in a string" + cube.attributes["z_tupular"] = (6, (7, 8)) + rep = cube_replines(cube) + # NOTE: 'list' before 'number', as it uses "sorted(attrs.items())" + expected = [ + "name / (1) (-- : 1)", + " Attributes:", + " list [3]", + " number 1.2", + " string four five in a string", + " z_tupular (6, (7, 8))", + ] + self.assertEqual(rep, expected) + + def test_section_cube_attributes__string_extras(self): + cube = Cube([0], long_name="name", units=1) + # Overlong strings are truncated (with iris.util.clip_string). + long_string = ( + "this is very very very very very very very " + "very very very very very very very long." + ) + # Strings with embedded newlines or quotes are printed in quoted form. + cube.attributes["escaped"] = "escaped\tstring" + cube.attributes["long"] = long_string + cube.attributes["long_multi"] = "multi\nline, " + long_string + rep = cube_replines(cube) + expected = [ + "name / (1) (-- : 1)", + " Attributes:", + " escaped 'escaped\\tstring'", + ( + " long this is very very very " + "very very very very very very very very very very..." + ), + ( + " long_multi 'multi\\nline, " + "this is very very very very very very very very very very..." + ), + ] + self.assertEqual(rep, expected) + + def test_section_cube_attributes__array(self): + # Including a long one, which gets a truncated representation. + cube = Cube([0], long_name="name", units=1) + small_array = np.array([1.2, 3.4]) + large_array = np.arange(36).reshape((18, 2)) + cube.attributes["array"] = small_array + cube.attributes["bigarray"] = large_array + rep = cube_replines(cube) + expected = [ + "name / (1) (-- : 1)", + " Attributes:", + " array array([1.2, 3.4])", + ( + " bigarray array([[ 0, 1], [ 2, 3], " + "[ 4, 5], [ 6, 7], [ 8, 9], [10, 11], [12, 13],..." + ), + ] + self.assertEqual(rep, expected) + + def test_section_cell_methods(self): + cube = Cube([0], long_name="name", units=1) + cube.add_cell_method(CellMethod("stdev", "area")) + cube.add_cell_method( + CellMethod( + method="mean", + coords=["y", "time"], + intervals=["10m", "3min"], + comments=["vertical", "=duration"], + ) + ) + rep = cube_replines(cube) + # Note: not alphabetical -- provided order is significant + expected = [ + "name / (1) (-- : 1)", + " Cell methods:", + " stdev area", + " mean y (10m, vertical), time (3min, =duration)", + ] + self.assertEqual(rep, expected) + + +if __name__ == "__main__": + tests.main() diff --git a/lib/iris/tests/unit/representation/cube_printout/test_Table.py b/lib/iris/tests/unit/representation/cube_printout/test_Table.py new file mode 100644 index 0000000000..2ff6738998 --- /dev/null +++ b/lib/iris/tests/unit/representation/cube_printout/test_Table.py @@ -0,0 +1,159 @@ +# 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. +"""Unit tests for :class:`iris._representation.cube_printout.Table`.""" +from iris._representation.cube_printout import Table +import iris.tests as tests + + +class TestTable(tests.IrisTest): + # Note: this is just barely an independent definition, not *strictly* part + # of CubePrinter, but effectively more-or-less so. + def setUp(self): + table = Table() + table.add_row(["one", "b", "three"], aligns=["left", "right", "left"]) + table.add_row(["a", "two", "c"], aligns=["right", "left", "right"]) + self.simple_table = table + + def test_empty(self): + table = Table() + self.assertIsNone(table.n_columns) + self.assertEqual(len(table.rows), 0) + self.assertIsNone(table.col_widths) + # Check other methods : should be ok but do nothing. + table.set_min_column_widths() # Ok but does nothing. + self.assertIsNone(table.col_widths) + self.assertEqual(table.formatted_as_strings(), []) + self.assertEqual(str(table), "") + + def test_basic_content(self): + # Mirror the above 'empty' tests on a small basic table. + table = self.simple_table + self.assertEqual(table.n_columns, 3) + self.assertEqual(len(table.rows), 2) + self.assertIsNone(table.col_widths) + table.set_min_column_widths() # Ok but does nothing. + self.assertEqual(table.col_widths, [3, 3, 5]) + self.assertEqual( + table.formatted_as_strings(), ["one b three", " a two c"] + ) + self.assertEqual(str(table), "one b three\n a two c") + + def test_copy(self): + table = self.simple_table + # Add some detail information + table.rows[1].i_col_unlimited = 77 # Doesn't actually affect anything + table.col_widths = [10, 15, 12] + # Make the copy + table2 = table.copy() + self.assertIsNot(table2, table) + self.assertNotEqual(table2, table) # Note: equality is not implemented + # Check the parts match the original. + self.assertEqual(len(table2.rows), len(table.rows)) + for row2, row in zip(table2.rows, table.rows): + self.assertEqual(row2.cols, row.cols) + self.assertEqual(row2.aligns, row.aligns) + self.assertEqual(row2.i_col_unlimited, row.i_col_unlimited) + + def test_add_row(self): + table = Table() + self.assertEqual(table.n_columns, None) + # Add onw row. + table.add_row(["one", "two", "three"], aligns=["left", "left", "left"]) + self.assertEqual(len(table.rows), 1) + self.assertEqual(table.n_columns, 3) + self.assertIsNone(table.rows[0].i_col_unlimited) + # Second row ok. + table.add_row( + ["x", "y", "z"], + aligns=["right", "right", "right"], + i_col_unlimited=199, + ) + self.assertEqual(len(table.rows), 2) + self.assertEqual(table.rows[-1].i_col_unlimited, 199) + + # Fails with bad number of columns + regex = "columns.*!=.*existing" + with self.assertRaisesRegex(ValueError, regex): + table.add_row(["1", "2"], ["left", "right"]) + + # Fails with bad number of aligns + regex = "aligns.*!=.*col" + with self.assertRaisesRegex(ValueError, regex): + table.add_row(["1", "2", "3"], ["left", "left", "left", "left"]) + + def test_formatted_as_strings(self): + # Test simple self-print is same as + table = Table() + aligns = ["left", "right", "left"] + table.add_row(["1", "266", "32"], aligns) + table.add_row(["123", "2", "3"], aligns) + + # Check that printing calculates default column widths, and result.. + self.assertEqual(table.col_widths, None) + result = table.formatted_as_strings() + self.assertEqual(result, ["1 266 32", "123 2 3"]) + self.assertEqual(table.col_widths, [3, 3, 2]) + + def test_fail_bad_alignments(self): + # Invalid 'aligns' content : only detected when printed + table = Table() + table.add_row(["1", "2", "3"], ["left", "right", "BAD"]) + regex = 'Unknown alignment "BAD"' + with self.assertRaisesRegex(ValueError, regex): + str(table) + + def test_table_set_width(self): + # Check that changes do *not* affect pre-existing widths. + table = Table() + aligns = ["left", "right", "left"] + table.col_widths = [3, 3, 2] + table.add_row(["333", "333", "22"], aligns) + table.add_row(["a", "b", "c"], aligns) + table.add_row(["12345", "12345", "12345"], aligns) + result = table.formatted_as_strings() + self.assertEqual(table.col_widths, [3, 3, 2]) + self.assertEqual( + result, + [ + "333 333 22", + "a b c", + "12345 12345 12345", # These are exceeding the given widths. + ], + ) + + def test_unlimited_column(self): + table = Table() + aligns = ["left", "right", "left"] + table.add_row(["a", "beee", "c"], aligns) + table.add_row( + ["abcd", "any-longer-stuff", "this"], aligns, i_col_unlimited=1 + ) + table.add_row(["12", "x", "yy"], aligns) + result = table.formatted_as_strings() + self.assertEqual( + result, + [ + "a beee c", + "abcd any-longer-stuff this", + # NOTE: the widths-calc is ignoring cols 1-2, but + # entry#0 *is* extending the width of col#0 + "12 x yy", + ], + ) + + def test_str(self): + # Check that str returns the formatted_as_strings() output. + table = Table() + aligns = ["left", "left", "left"] + table.add_row(["one", "two", "three"], aligns=aligns) + table.add_row(["1", "2", "3"], aligns=aligns) + expected = "\n".join(table.formatted_as_strings()) + result = str(table) + self.assertEqual(result, expected) + + +if __name__ == "__main__": + tests.main() diff --git a/lib/iris/tests/unit/representation/cube_summary/__init__.py b/lib/iris/tests/unit/representation/cube_summary/__init__.py new file mode 100644 index 0000000000..c20a621ba2 --- /dev/null +++ b/lib/iris/tests/unit/representation/cube_summary/__init__.py @@ -0,0 +1,6 @@ +# 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. +"""Unit tests for the :mod:`iris._representation.cube_summary` module.""" diff --git a/lib/iris/tests/unit/representation/test_representation.py b/lib/iris/tests/unit/representation/cube_summary/test_CubeSummary.py similarity index 93% rename from lib/iris/tests/unit/representation/test_representation.py rename to lib/iris/tests/unit/representation/cube_summary/test_CubeSummary.py index 238772a10c..79baf65c8b 100644 --- a/lib/iris/tests/unit/representation/test_representation.py +++ b/lib/iris/tests/unit/representation/cube_summary/test_CubeSummary.py @@ -3,7 +3,7 @@ # 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. -"""Unit tests for the :mod:`iris._representation` module.""" +"""Unit tests for :class:`iris._representation.cube_summary.CubeSummary`.""" # Import iris.tests first so that some things can be initialised before # importing anything else. @@ -11,7 +11,7 @@ import numpy as np -import iris._representation +from iris._representation.cube_summary import CubeSummary from iris.coords import ( AncillaryVariable, AuxCoord, @@ -40,7 +40,7 @@ def setUp(self): self.cube = example_cube() def test_header(self): - rep = iris._representation.CubeSummary(self.cube) + rep = CubeSummary(self.cube) header_left = rep.header.nameunit header_right = rep.header.dimension_header.contents @@ -49,7 +49,7 @@ def test_header(self): def test_blank_cube(self): cube = Cube([1, 2]) - rep = iris._representation.CubeSummary(cube) + rep = CubeSummary(cube) self.assertEqual(rep.header.nameunit, "unknown / (unknown)") self.assertEqual(rep.header.dimension_header.contents, ["-- : 2"]) @@ -72,8 +72,8 @@ def test_blank_cube(self): expected_scalar_sections = [ "Scalar coordinates:", "Scalar cell measures:", - "Attributes:", "Cell methods:", + "Attributes:", ] self.assertEqual( @@ -85,7 +85,7 @@ def test_blank_cube(self): self.assertTrue(scalar_section.is_empty()) def test_vector_coord(self): - rep = iris._representation.CubeSummary(self.cube) + rep = CubeSummary(self.cube) dim_section = rep.vector_sections["Dimension coordinates:"] self.assertEqual(len(dim_section.contents), 1) @@ -119,7 +119,7 @@ def test_scalar_coord(self): cube.add_aux_coord(scalar_coord_with_bounds) cube.add_aux_coord(scalar_coord_simple_text) cube.add_aux_coord(scalar_coord_awkward_text) - rep = iris._representation.CubeSummary(cube) + rep = CubeSummary(cube) scalar_section = rep.scalar_sections["Scalar coordinates:"] @@ -152,7 +152,7 @@ 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) + rep = CubeSummary(cube) cm_section = rep.vector_sections["Cell measures:"] self.assertEqual(len(cm_section.contents), 1) @@ -165,7 +165,7 @@ def test_ancillary_variable(self): cube = self.cube cell_measure = AncillaryVariable([1, 2, 3], long_name="foo") cube.add_ancillary_variable(cell_measure, 0) - rep = iris._representation.CubeSummary(cube) + rep = CubeSummary(cube) av_section = rep.vector_sections["Ancillary variables:"] self.assertEqual(len(av_section.contents), 1) @@ -177,7 +177,7 @@ def test_ancillary_variable(self): def test_attributes(self): cube = self.cube cube.attributes = {"a": 1, "b": "two", "c": " this \n that\tand."} - rep = iris._representation.CubeSummary(cube) + rep = CubeSummary(cube) attribute_section = rep.scalar_sections["Attributes:"] attribute_contents = attribute_section.contents @@ -196,7 +196,7 @@ def test_cell_methods(self): cube.add_cell_method(cell_method_xy) cube.add_cell_method(cell_method_x) - rep = iris._representation.CubeSummary(cube) + rep = CubeSummary(cube) cell_method_section = rep.scalar_sections["Cell methods:"] expected_contents = ["mean: x, y", "mean: x"] self.assertEqual(cell_method_section.contents, expected_contents) @@ -205,7 +205,7 @@ def test_scalar_cube(self): cube = self.cube while cube.ndim > 0: cube = cube[0] - rep = iris._representation.CubeSummary(cube) + rep = 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, []) @@ -232,7 +232,7 @@ def test_coord_attributes(self): 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) + rep = 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. @@ -248,7 +248,7 @@ def test_array_attributes(self): 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) + rep = 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. ])") @@ -291,7 +291,7 @@ def test_attributes_subtle_differences(self): for co in (co3a, co3b): cube.add_aux_coord(co) - rep = iris._representation.CubeSummary(cube) + rep = 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])") diff --git a/lib/iris/util.py b/lib/iris/util.py index 9cadf85f0f..54e18548c5 100644 --- a/lib/iris/util.py +++ b/lib/iris/util.py @@ -1540,23 +1540,23 @@ def promote_aux_coord_to_dim_coord(cube, name_or_coord): >>> print(cube) air_temperature / (K) (time: 240; latitude: 37; longitude: 49) - Dimension coordinates: - time x - - - latitude - x - - longitude - - x - Auxiliary coordinates: - forecast_period x - - - year x - - + Dimension coordinates: + time x - - + latitude - x - + longitude - - x + Auxiliary coordinates: + forecast_period x - - + year x - - >>> promote_aux_coord_to_dim_coord(cube, "year") >>> print(cube) air_temperature / (K) (year: 240; latitude: 37; longitude: 49) - Dimension coordinates: - year x - - - latitude - x - - longitude - - x - Auxiliary coordinates: - forecast_period x - - - time x - - + Dimension coordinates: + year x - - + latitude - x - + longitude - - x + Auxiliary coordinates: + forecast_period x - - + time x - - """ from iris.coords import Coord, DimCoord @@ -1666,23 +1666,23 @@ def demote_dim_coord_to_aux_coord(cube, name_or_coord): >>> print(cube) air_temperature / (K) (time: 240; latitude: 37; longitude: 49) - Dimension coordinates: - time x - - - latitude - x - - longitude - - x - Auxiliary coordinates: - forecast_period x - - - year x - - + Dimension coordinates: + time x - - + latitude - x - + longitude - - x + Auxiliary coordinates: + forecast_period x - - + year x - - >>> demote_dim_coord_to_aux_coord(cube, "time") >>> print(cube) air_temperature / (K) (-- : 240; latitude: 37; longitude: 49) - Dimension coordinates: - latitude - x - - longitude - - x - Auxiliary coordinates: - forecast_period x - - - time x - - - year x - - + Dimension coordinates: + latitude - x - + longitude - - x + Auxiliary coordinates: + forecast_period x - - + time x - - + year x - - """ from iris.coords import Coord