Skip to content
Open
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
5 changes: 4 additions & 1 deletion docs/src/whatsnew/latest.rst
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,10 @@ This document explains the changes made to Iris for this release
🐛 Bugs Fixed
=============

#. N/A
#. :user:`gaoflow` made :class:`iris.Constraint` raise a ``TypeError`` when used
in a boolean context (e.g. with the ``and``/``or``/``not`` keywords) instead
of silently discarding one of the constraints. Use the ``&`` operator to
combine constraints. (:issue:`4337`)


💣 Incompatible Changes
Expand Down
12 changes: 12 additions & 0 deletions lib/iris/_constraints.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,18 @@ def __and__(self, other):
def __rand__(self, other):
return ConstraintCombination(other, self, operator.__and__)

def __bool__(self):
# Constraints have no truth value: combining them with the Python
# keywords ``and``/``or``/``not`` (which call bool()) silently returns
# one of the operands instead of a combined Constraint, losing the
# other. Raise an explanatory error so this is not a silent failure;
# use the ``&`` operator to combine constraints. See #4337.
raise TypeError(
"The truth value of a Constraint is ambiguous. Constraints cannot "
"be combined with the 'and', 'or' and 'not' keywords; use the '&' "
"operator instead, e.g. 'constraint1 & constraint2'."
)


class ConstraintCombination(Constraint):
"""Represents the binary combination of two Constraint instances."""
Expand Down
68 changes: 68 additions & 0 deletions lib/iris/tests/unit/constraints/test_Constraint__bool__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Copyright Iris contributors
#
# This file is part of Iris and is released under the BSD license.
# See LICENSE in the root of the repository for full licensing details.
"""Unit tests for :meth:`iris._constraints.Constraint.__bool__`."""

import operator

import pytest

from iris._constraints import (
AttributeConstraint,
Constraint,
ConstraintCombination,
NameConstraint,
)


class Test_Constraint__bool__:
# Using a Constraint in a boolean context (e.g. with the ``and``/``or``/
# ``not`` keywords) used to silently return one of the operands, quietly
# discarding the other. It should instead raise an informative TypeError
# so the user is directed towards the ``&`` operator (see #4337).

_match = "truth value of a Constraint is ambiguous"

def test_bool(self):
with pytest.raises(TypeError, match=self._match):
bool(Constraint("air_temperature"))

def test_keyword_or(self):
c1 = Constraint("air_temperature")
c2 = Constraint("time")
with pytest.raises(TypeError, match=self._match):
c1 or c2

def test_keyword_and(self):
c1 = Constraint("air_temperature")
c2 = Constraint("time")
with pytest.raises(TypeError, match=self._match):
c1 and c2

def test_keyword_not(self):
with pytest.raises(TypeError, match=self._match):
not Constraint("air_temperature")

def test_constraint_combination(self):
combination = ConstraintCombination(
Constraint("air_temperature"),
Constraint("time"),
operator.__and__,
)
with pytest.raises(TypeError, match=self._match):
bool(combination)

def test_attribute_constraint(self):
with pytest.raises(TypeError, match=self._match):
bool(AttributeConstraint(STASH="m01s00i024"))

def test_name_constraint(self):
with pytest.raises(TypeError, match=self._match):
bool(NameConstraint(standard_name="air_temperature"))

def test_and_operator_still_works(self):
# The supported way of combining constraints must be unaffected.
c1 = Constraint("air_temperature")
c2 = Constraint("time")
assert isinstance(c1 & c2, ConstraintCombination)
Loading