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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion lib/iris/_concatenate.py
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,11 @@ def concatenate(cubes, error_on_mismatch=False):
# Construct a concatenated cube from each of the proto-cubes.
concatenated_cubes = iris.cube.CubeList()

for name in sorted(proto_cubes_by_name):
# Emulate Python 2 behaviour.
def _none_sort(item):
return (item is not None, item)

for name in sorted(proto_cubes_by_name, key=_none_sort):
for proto_cube in proto_cubes_by_name[name]:
# Construct the concatenated cube.
concatenated_cubes.append(proto_cube.concatenate())
Expand Down
4 changes: 3 additions & 1 deletion lib/iris/_merge.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

from __future__ import (absolute_import, division, print_function)
from six.moves import (filter, input, map, range, zip) # noqa
import six

from collections import namedtuple, OrderedDict
from copy import deepcopy
Expand Down Expand Up @@ -1300,7 +1301,8 @@ def _define_space(self, space, positions, indexes, function_matrix):
def axis_and_name(name):
axis_dict = {'T': 1, 'Z': 2, 'Y': 3, 'X': 4}
axis_index = axis_dict.get(self._guess_axis(name), 0)
return (axis_index, name)
# The middle element ensures sorting is the same as Python 2.
return (axis_index, not isinstance(name, six.integer_types), name)
names = sorted(space, key=axis_and_name)
dim_by_name = {}

Expand Down
14 changes: 14 additions & 0 deletions lib/iris/coords.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,20 @@ def name(self, default='unknown'):
"""
return self.standard_name or self.long_name or self.var_name or default

def __lt__(self, other):
if not isinstance(other, CoordDefn):
return NotImplemented

def _sort_key(defn):
# Emulate Python 2 behaviour with None
return (defn.standard_name is not None, defn.standard_name,
defn.long_name is not None, defn.long_name,
defn.var_name is not None, defn.var_name,
defn.units is not None, defn.units,
defn.coord_system is not None, defn.coord_system)

return _sort_key(self) < _sort_key(other)


class CoordExtent(collections.namedtuple('_CoordExtent', ['name_or_coord',
'minimum',
Expand Down
6 changes: 5 additions & 1 deletion lib/iris/cube.py
Original file line number Diff line number Diff line change
Expand Up @@ -452,9 +452,13 @@ def merge(self, unique=True):
proto_cube = iris._merge.ProtoCube(cube)
proto_cubes.append(proto_cube)

# Emulate Python 2 behaviour.
def _none_sort(item):
return (item is not None, item)

# Extract all the merged cubes from the ProtoCubes.
merged_cubes = CubeList()
for name in sorted(proto_cubes_by_name):
for name in sorted(proto_cubes_by_name, key=_none_sort):
for proto_cube in proto_cubes_by_name[name]:
merged_cubes.extend(proto_cube.merge(unique=unique))

Expand Down
7 changes: 5 additions & 2 deletions lib/iris/io/format_picker.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,17 +211,20 @@ def handler(self):
"""The handler function of this FileFormat. (Read only)"""
return self._handler

def _sort_key(self):
return (-self.priority, self.name, self.file_element)

def __lt__(self, other):
if not isinstance(other, FormatSpecification):
return NotImplemented

return (-self.priority, hash(self)) < (-other.priority, hash(other))
return self._sort_key() < other._sort_key()

def __eq__(self, other):
if not isinstance(other, FormatSpecification):
return NotImplemented

return self.priority == other.priority and hash(self) == hash(other)
return self._sort_key() == other._sort_key()

def __ne__(self, other):
return not (self == other)
Expand Down
16 changes: 8 additions & 8 deletions lib/iris/tests/results/file_load/known_loaders.txt
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
* NetCDF OPeNDAP (priority 6)
* NAME III (priority 5)
* NetCDF_v4 (priority 5)
* UM Post Processing file (PP) (priority 5)
* NetCDF (priority 5)
* NetCDF 64 bit offset format (priority 5)
* NetCDF_v4 (priority 5)
* UM Post Processing file (PP) (priority 5)
Copy link
Member

Choose a reason for hiding this comment

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

We don't really want this list changing order ... that indicates a potential change in behaviour.

Copy link
Member

Choose a reason for hiding this comment

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

The order comes from the FormatSpecification comparitor:

    def __lt__(self, other):
        if not isinstance(other, FormatSpecification):
            return NotImplemented

        return (-self.priority, hash(self)) < (-other.priority, hash(other))

Eww 😱

Copy link
Member

Choose a reason for hiding this comment

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

Perhaps the simplest solution is to tweak the definitions in lib/iris/fileformats/__init__.py to use unique numbers that preserve the current order?

Copy link
Member Author

Choose a reason for hiding this comment

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

I assumed (perhaps incorrectly) that as long as they are at the same priority, it should not matter which one is ordered first. I don't think the tests have complained so far...

Copy link
Member

Choose a reason for hiding this comment

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

Format specifications with the same priority should have mutually exclusive file matching criteria, in which case we would be able to get away with it. I'll check...

Copy link
Member

Choose a reason for hiding this comment

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

Mostly they are mutually exclusive, but sadly not in the case of the ABF/L formats vs. the other "priority 3" formats. For example, a FieldsFile named "foo.abf" would load OK using the old order, but would try to load as an ABF file using the new order.

Copy link
Member

Choose a reason for hiding this comment

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

NB. We really shouldn't be relying on consistent hash results anyway - e.g. http://bugs.python.org/issue13703.

Copy link
Member

Choose a reason for hiding this comment

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

Given the minimal impact I suggest we just accept the change as it makes the order more predictable and controllable.

* UM Fieldsfile (FF) post v5.2 (priority 4)
* UM Fieldsfile (FF) pre v3.1 (priority 3)
* UM Fieldsfile (FF) ancillary (priority 3)
* UM Post Processing file (PP) little-endian (priority 3)
* UM Fieldsfile (FF) converted with ieee to 32 bit (priority 3)
* UM Fieldsfile (FF) ancillary converted with ieee to 32 bit (priority 3)
* NIMROD (priority 3)
* ABF (priority 3)
* ABL (priority 3)
* NIMROD (priority 3)
* UM Fieldsfile (FF) ancillary (priority 3)
* UM Fieldsfile (FF) ancillary converted with ieee to 32 bit (priority 3)
* UM Fieldsfile (FF) converted with ieee to 32 bit (priority 3)
* UM Fieldsfile (FF) pre v3.1 (priority 3)
* UM Post Processing file (PP) little-endian (priority 3)
* GRIB (priority 1)
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,8 @@ def assert_bounded_message(self, **kwargs):
'edition': 1, '_forecastTime': 15,
'_forecastTimeUnit': 'hours',
'phenomenon_bounds': lambda u: (80, 120),
'_phenomenonDateTime': -1}
'_phenomenonDateTime': -1,
'table2Version': 9999}
Copy link
Member

Choose a reason for hiding this comment

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

Is this change supposed to be here?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, somewhere in the GRIB loader is if grib.table2Version < 128: which doesn't work with a Mock object (not exactly sorting, but related to comparison operators.)

Copy link
Member

Choose a reason for hiding this comment

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

OK - thanks for clarifying. 👍

attributes.update(kwargs)
message = mock.Mock(**attributes)
self._test_for_coord(message, convert, self.is_forecast_period,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,9 +118,9 @@ def test_other_lbtim_ib(self):
self.assertEqual(res, expected)

def test_multiple_unordered_lbprocs(self):
field = mock.MagicMock(lbproc=192,
field = mock.MagicMock(lbproc=192, bzx=0, bdx=1, lbnpt=3, lbrow=3,
lbtim=mock.Mock(ia=24, ib=5, ic=3),
lbcode=SplittableInt(1),
lbcode=SplittableInt(1), x_bounds=None,
_x_coord_name=lambda: 'longitude',
_y_coord_name=lambda: 'latitude')
res = _all_other_rules(field)[CELL_METHODS_INDEX]
Expand All @@ -129,9 +129,9 @@ def test_multiple_unordered_lbprocs(self):
self.assertEqual(res, expected)

def test_multiple_unordered_rotated_lbprocs(self):
field = mock.MagicMock(lbproc=192,
field = mock.MagicMock(lbproc=192, bzx=0, bdx=1, lbnpt=3, lbrow=3,
lbtim=mock.Mock(ia=24, ib=5, ic=3),
lbcode=SplittableInt(101),
lbcode=SplittableInt(101), x_bounds=None,
_x_coord_name=lambda: 'grid_longitude',
_y_coord_name=lambda: 'grid_latitude')
res = _all_other_rules(field)[CELL_METHODS_INDEX]
Expand Down