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
7 changes: 7 additions & 0 deletions docs/src/whatsnew/latest.rst
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,13 @@ This document explains the changes made to Iris for this release
a new error class: :class:`~iris.exceptions.CannotAddError` (subclass of
:class:`ValueError`). (:pull:`5054`)

#. `@pp-mo`_ implemented == and != comparisons for :class:`~iris.Constraint` s.
A simple constraint is now == to another one constructed in the same way.
However, equality is limited for more complex cases : value-matching functions must
be the same identical function, and for &-combinations order is significant,
i.e. ``(c1 & c2) != (c2 & c1)``.
(:issue:`3616`, :pull:`3749`).


🐛 Bugs Fixed
=============
Expand Down
71 changes: 71 additions & 0 deletions lib/iris/_constraints.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,30 @@ def latitude_bands(cell):
_CoordConstraint(coord_name, coord_thing)
)

def __eq__(self, other):
# Equivalence is defined, but is naturally limited for any Constraints
# based on callables, i.e. "cube_func", or value functions for
# attributes/names/coords : These can only be == if they contain the
# *same* callable object (i.e. same object identity).
eq = (
type(other) == Constraint
and self._name == other._name
and self._cube_func == other._cube_func
and self._coord_constraints == other._coord_constraints
)
# NOTE: theoretically, you could compare coord constraints as a *set*,
# as order should not affect matching.
# Not totally sure, so for now let's not.
return eq

def __hash__(self):
# We want constraints to have hashes, so they can act as e.g.
# dictionary keys or tuple elements.
# So, we *must* provide this, as overloading '__eq__' automatically
# disables it.
# Just use basic object identity.
return id(self)

def __repr__(self):
args = []
if self._name:
Expand Down Expand Up @@ -218,6 +242,19 @@ def __init__(self, lhs, rhs, operator):
self.rhs = rhs_constraint
self.operator = operator

def __eq__(self, other):
eq = (
type(other) == ConstraintCombination
and self.lhs == other.lhs
and self.rhs == other.rhs
and self.operator == other.operator
)
return eq

def __hash__(self):
# Must re-define if you overload __eq__ : Use object identity.
return id(self)

def _coordless_match(self, cube):
return self.operator(
self.lhs._coordless_match(cube), self.rhs._coordless_match(cube)
Expand Down Expand Up @@ -261,6 +298,18 @@ def __repr__(self):
self._coord_thing,
)

def __eq__(self, other):
eq = (
type(other) == _CoordConstraint
and self.coord_name == other.coord_name
and self._coord_thing == other._coord_thing
)
return eq

def __hash__(self):
# Must re-define if you overload __eq__ : Use object identity.
return id(self)

def extract(self, cube):
"""
Returns the the column based indices of the given cube which
Expand Down Expand Up @@ -493,6 +542,17 @@ def __init__(self, **attributes):
self._attributes = attributes
super().__init__(cube_func=self._cube_func)

def __eq__(self, other):
eq = (
type(other) == AttributeConstraint
and self._attributes == other._attributes
)
return eq

def __hash__(self):
# Must re-define if you overload __eq__ : Use object identity.
return id(self)

def _cube_func(self, cube):
match = True
for name, value in self._attributes.items():
Expand Down Expand Up @@ -577,6 +637,17 @@ def __init__(
self._names = ("standard_name", "long_name", "var_name", "STASH")
super().__init__(cube_func=self._cube_func)

def __eq__(self, other):
eq = type(other) == NameConstraint and all(
getattr(self, attname) == getattr(other, attname)
for attname in self._names
)
return eq

def __hash__(self):
# Must re-define if you overload __eq__ : Use object identity.
return id(self)

def _cube_func(self, cube):
def matcher(target, value):
if callable(value):
Expand Down
274 changes: 274 additions & 0 deletions lib/iris/tests/unit/constraints/test_Constraint_equality.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
# 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 equality testing of different constraint types."""

# Import iris.tests first so that some things can be initialised before
# importing anything else.
import iris.tests as tests # isort:skip

from iris._constraints import AttributeConstraint, Constraint, NameConstraint


class Test_Constraint__hash__(tests.IrisTest):
def test_empty(self):
c1 = Constraint()
c2 = Constraint()
self.assertEqual(hash(c1), hash(c1))
self.assertNotEqual(hash(c1), hash(c2))


class Test_Constraint__eq__(tests.IrisTest):
def test_empty_same(self):
c1 = Constraint()
c2 = Constraint()
self.assertEqual(c1, c2)
self.assertIsNot(c1, c2)

def test_emptyname_same(self):
c1 = Constraint("")
c2 = Constraint("")
self.assertEqual(c1, c2)

def test_empty_emptyname_differ(self):
c1 = Constraint()
c2 = Constraint("")
self.assertNotEqual(c1, c2)

def test_names_same(self):
c1 = Constraint("a")
c2 = Constraint("a")
self.assertEqual(c1, c2)

def test_names_differ(self):
c1 = Constraint("a")
c2 = Constraint("b")
self.assertNotEqual(c1, c2)

def test_funcs_same(self):
# *Same* functions match
def func(cube):
return False

c1 = Constraint(cube_func=func)
c2 = Constraint(cube_func=func)
self.assertEqual(c1, c2)

def test_funcs_differ(self):
# Identical but different funcs do not match.
c1 = Constraint(cube_func=lambda c: False)
c2 = Constraint(cube_func=lambda c: False)
self.assertNotEqual(c1, c2)

def test_coord_names_same(self):
c1 = Constraint(some_coordname=3)
c2 = Constraint(some_coordname=3)
self.assertEqual(c1, c2)

def test_coord_names_differ(self):
c1 = Constraint(some_coordname_A=3)
c2 = Constraint(some_coordname_B=3)
self.assertNotEqual(c1, c2)

def test_coord_values_differ(self):
c1 = Constraint(some_coordname=3)
c2 = Constraint(some_coordname=4)
self.assertNotEqual(c1, c2)

def test_coord_orders_differ(self):
# We *could* maybe ignore Coordinate order, but at present we don't.
c1 = Constraint(coordname_1=1, coordname_2=2)
c2 = Constraint(coordname_2=2, coordname_1=1)
self.assertNotEqual(c1, c2)

def test_coord_values_functions_same(self):
def func(coord):
return False

c1 = Constraint(some_coordname=func)
c2 = Constraint(some_coordname=func)
self.assertEqual(c1, c2)

def test_coord_values_functions_differ(self):
# Identical functions are not the same.
c1 = Constraint(some_coordname=lambda c: True)
c2 = Constraint(some_coordname=lambda c: True)
self.assertNotEqual(c1, c2)

def test_coord_values_and_keys_same(self):
# **kwargs and 'coord_values=' are combined without distinction.
c1 = Constraint(coord_values={"a": [2, 3]})
c2 = Constraint(a=[2, 3])
self.assertEqual(c1, c2)


class Test_AttributeConstraint__hash__(tests.IrisTest):
def test_empty(self):
c1 = AttributeConstraint()
c2 = AttributeConstraint()
self.assertEqual(hash(c1), hash(c1))
self.assertNotEqual(hash(c1), hash(c2))


class Test_AttributeConstraint__eq__(tests.IrisTest):
def test_empty_same(self):
c1 = AttributeConstraint()
c2 = AttributeConstraint()
self.assertEqual(c1, c2)
self.assertIsNot(c1, c2)

def test_attribute_plain_empty_diff(self):
c1 = AttributeConstraint()
c2 = Constraint()
self.assertNotEqual(c1, c2)

def test_names_same(self):
c1 = AttributeConstraint(a=1)
c2 = AttributeConstraint(a=1)
self.assertEqual(c1, c2)

def test_names_diff(self):
c1 = AttributeConstraint(a=1)
c2 = AttributeConstraint(a=1, b=1)
self.assertNotEqual(c1, c2)

def test_values_diff(self):
c1 = AttributeConstraint(a=1, b=1)
c2 = AttributeConstraint(a=1, b=2)
self.assertNotEqual(c1, c2)

def test_func_same(self):
def func(attrs):
return False

c1 = AttributeConstraint(a=func)
c2 = AttributeConstraint(a=func)
self.assertEqual(c1, c2)

def test_func_diff(self):
c1 = AttributeConstraint(a=lambda a: False)
c2 = AttributeConstraint(a=lambda a: False)
self.assertNotEqual(c1, c2)


class Test_NameConstraint__hash__(tests.IrisTest):
def test_empty(self):
c1 = NameConstraint()
c2 = NameConstraint()
self.assertEqual(hash(c1), hash(c1))
self.assertNotEqual(hash(c1), hash(c2))


class Test_NameConstraint__eq__(tests.IrisTest):
def test_empty_same(self):
c1 = NameConstraint()
c2 = NameConstraint()
self.assertEqual(c1, c2)
self.assertIsNot(c1, c2)

def test_attribute_plain_empty_diff(self):
c1 = NameConstraint()
c2 = Constraint()
self.assertNotEqual(c1, c2)

def test_names_same(self):
c1 = NameConstraint(standard_name="air_temperature")
c2 = NameConstraint(standard_name="air_temperature")
self.assertEqual(c1, c2)

def test_full_same(self):
c1 = NameConstraint(
standard_name="air_temperature",
long_name="temp",
var_name="tair",
STASH="m01s02i003",
)
c2 = NameConstraint(
standard_name="air_temperature",
long_name="temp",
var_name="tair",
STASH="m01s02i003",
)
self.assertEqual(c1, c2)

def test_missing_diff(self):
c1 = NameConstraint(standard_name="air_temperature", var_name="tair")
c2 = NameConstraint(standard_name="air_temperature")
self.assertNotEqual(c1, c2)

def test_standard_name_diff(self):
c1 = NameConstraint(standard_name="air_temperature")
c2 = NameConstraint(standard_name="height")
self.assertNotEqual(c1, c2)

def test_long_name_diff(self):
c1 = NameConstraint(long_name="temp")
c2 = NameConstraint(long_name="t3")
self.assertNotEqual(c1, c2)

def test_var_name_diff(self):
c1 = NameConstraint(var_name="tair")
c2 = NameConstraint(var_name="xxx")
self.assertNotEqual(c1, c2)

def test_stash_diff(self):
c1 = NameConstraint(STASH="m01s02i003")
c2 = NameConstraint(STASH="m01s02i777")
self.assertNotEqual(c1, c2)

def test_func_same(self):
def func(name):
return True

c1 = NameConstraint(STASH="m01s02i003", long_name=func)
c2 = NameConstraint(STASH="m01s02i003", long_name=func)
self.assertEqual(c1, c2)

def test_func_diff(self):
c1 = NameConstraint(STASH="m01s02i003", long_name=lambda n: True)
c2 = NameConstraint(STASH="m01s02i003", long_name=lambda n: True)
self.assertNotEqual(c1, c2)


class Test_ConstraintCombination__hash__(tests.IrisTest):
def test_empty(self):
c1 = Constraint() & Constraint()
c2 = Constraint() & Constraint()
self.assertEqual(hash(c1), hash(c1))
self.assertNotEqual(hash(c1), hash(c2))

def test_identical_construction(self):
c1, c2 = Constraint(a=1), Constraint(b=1)
cc1 = c1 & c2
cc2 = c1 & c2
self.assertNotEqual(hash(cc1), hash(cc2))


class Test_ConstraintCombination__eq__(tests.IrisTest):
def test_empty_same(self):
c1 = Constraint() & Constraint()
c2 = Constraint() & Constraint()
self.assertEqual(c1, c2)
self.assertIsNot(c1, c2)

def test_multi_components_same(self):
c1 = Constraint("a") & Constraint(b=1)
c2 = Constraint("a") & Constraint(b=1)
self.assertEqual(c1, c2)

def test_multi_components_diff(self):
c1 = Constraint("a") & Constraint(b=1, c=2)
c2 = Constraint("a") & Constraint(b=1)
self.assertNotEqual(c1, c2)

def test_different_component_order(self):
c1, c2 = Constraint("a"), Constraint(b=1)
cc1 = c1 & c2
cc2 = c2 & c1
self.assertNotEqual(cc1, cc2)


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