Created
June 12, 2026 11:09
-
-
Save atifaziz/b6e4615197abd67acb6aa1b082dd2c87 to your computer and use it in GitHub Desktop.
Match!
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # MIT License | |
| # | |
| # Copyright (c) Microsoft Corporation. | |
| # | |
| # Permission is hereby granted, free of charge, to any person obtaining a copy | |
| # of this software and associated documentation files (the "Software"), to deal | |
| # in the Software without restriction, including without limitation the rights | |
| # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
| # copies of the Software, and to permit persons to whom the Software is | |
| # furnished to do so, subject to the following conditions: | |
| # | |
| # The above copyright notice and this permission notice shall be included in all | |
| # copies or substantial portions of the Software. | |
| # | |
| # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
| # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
| # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
| # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
| # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
| # SOFTWARE | |
| from collections.abc import Callable, Iterable, Sized | |
| from typing import Any, Final | |
| from unittest import TestCase | |
| class _Match: | |
| """ | |
| Represents a matching condition to implement relaxed and custom equality | |
| semantics. An instance of this class will compare equal to its right-hand | |
| value or comparand if it satisfies a given condition. The condition is | |
| supplied during initialisation and summoned when `__eq__` is invoked. The | |
| condition is a function (or any callable) that is supplied the value of | |
| the `other` argument of `__eq__` and returns a Boolean indicating whether | |
| the match is satisfied. | |
| The condition is immutable once created and various methods permit | |
| combining conditions together to form more complex conditions. The `any` | |
| instance is special and matches any value of any type: | |
| >>> print(any == 42) # True | |
| >>> print(any == 43) # True | |
| >>> print(any == "foo") # True | |
| >>> print(any == [1, 2, 3]) # True | |
| The `any` instance is the starting point for building more complex | |
| matching conditions. For example: | |
| >>> even = any.where(lambda x: x % 2 == 0) # Equal to any even integer | |
| >>> print(even == 42) # True | |
| >>> print(even == 43) # False | |
| You can also continue to build on top of existing match conditions, such | |
| in the next example that negates the even condition from the previous | |
| example using `not_()` to match odd integers: | |
| >>> odd = even.not_() | |
| >>> print(odd == 42) # False | |
| >>> print(odd == 43) # True | |
| You can also combine many conditions together: | |
| >>> negative = any.where(lambda x: x < 0) | |
| >>> positive = any.where(lambda x: x > 0) | |
| >>> zero = negative.not_().and_(positive.not_()) | |
| >>> # Above is for illustration only and logically equal to doing: | |
| >>> # zero = any.eq(0) | |
| >>> print(negative == -42) # True | |
| >>> print(negative == 42) # False | |
| >>> print(negative == 0) # False | |
| >>> print(positive == -42) # False | |
| >>> print(positive == 42) # True | |
| >>> print(positive == 0) # False | |
| >>> print(zero == -42) # False | |
| >>> print(zero == 42) # False | |
| >>> print(zero == 0) # True | |
| """ | |
| def __init__(self, condition: Callable[[Any], bool]) -> None: | |
| """Initialises this instance with the condition callable/function.""" | |
| self._condition = condition | |
| def __eq__(self, other: Any) -> bool: | |
| """Provides equality semantics based on the condition supplied during initialisation.""" | |
| return self._condition(other) | |
| def and_(self, other: "_Match") -> "_Match": | |
| """Adds another condition to match.""" | |
| return _Match(lambda x: self == x and other == x) | |
| def where(self, condition: Callable[[Any], bool]) -> "_Match": | |
| """Adds another condition, specifically a predicate function, to match.""" | |
| return self.and_(_Match(lambda x: condition(x))) | |
| def not_(self) -> "_Match": | |
| """Inverts this condition.""" | |
| return _Match(lambda x: not self._condition(x)) | |
| def of_type(self, type_: type) -> "_Match": | |
| """Matches only values of a given type.""" | |
| return self.where(lambda x: isinstance(x, type_)) | |
| def none(self) -> "_Match": | |
| """Matches only `None`.""" | |
| return self.where(lambda x: x is None) | |
| def not_none(self) -> "_Match": | |
| """Matches anything but `None`.""" | |
| return self.none().not_() | |
| def eq(self, value: Any) -> "_Match": | |
| """Matches only a given value.""" | |
| return self.where(lambda x: x == value) | |
| def ne(self, value: Any) -> "_Match": | |
| """Matches anything but a given value.""" | |
| return self.eq(value).not_() | |
| def in_(self, *values: Any) -> "_Match": | |
| """Matches any value contained in the given set of values.""" | |
| return self.where(lambda x: x in values) | |
| def having(self, f: Callable[[Any], Any], match: "_Match") -> "_Match": | |
| """Matches only values whose projection satisfies a given condition to match.""" | |
| return self.and_(_Match(lambda x: f(x) == match)) | |
| def len(self, value: int) -> "_Match": | |
| """Matches only (sized) values whose length is of the given value.""" | |
| return self.of_type(Sized).having(len, any.eq(value)) | |
| def count_equal(self, *values: Any) -> "_Match": | |
| """Wraps and behaves exactly like `TestCase.assertCountEqual`.""" | |
| def count_equal(actual: Iterable[Any]) -> bool: | |
| try: | |
| TestCase().assertCountEqual(actual, values) | |
| return True | |
| except AssertionError: | |
| return False | |
| return self.of_type(Iterable).where(count_equal) | |
| def non_empty_list(self): | |
| """Matches any list that is not empty.""" | |
| return self.and_(_NON_EMPTY_LIST) | |
| def either(a: _Match, b: _Match) -> _Match: | |
| """Matches any value that satisfies the conditions of either of the given matches.""" | |
| return _Match(lambda x: a == x or b == x) | |
| any = _Match(lambda _: True) # Matches anything | |
| _NON_EMPTY_LIST: Final[_Match] = any.of_type(list).having(len, any.ne(0)) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| from collections.abc import Callable | |
| from typing import Any, Final | |
| from unittest.mock import MagicMock as Mock | |
| import pytest | |
| from .match import any, either | |
| class Custom: | |
| pass | |
| # The following sample set contains values of various types except `None`.` | |
| SOME_SAMPLES: Final = [ | |
| True, | |
| False, | |
| 42, | |
| 4.2, | |
| "string", | |
| pytest.param([1, 2, 3], id="list"), | |
| pytest.param({"foo": 1, "bar": 2, "baz": 3}, id="dict"), | |
| pytest.param((1, 2, 3), id="tuple"), | |
| pytest.param(Custom(), id="custom"), | |
| ] | |
| SAMPLES: Final = [None] + SOME_SAMPLES | |
| @pytest.mark.parametrize( | |
| "comparand", | |
| SAMPLES, | |
| ) | |
| def test_any(comparand: Any) -> None: | |
| assert any == comparand | |
| class NonSampled: | |
| pass | |
| NON_SAMPLED: Final = NonSampled() | |
| @pytest.mark.parametrize( | |
| "comparand", | |
| SAMPLES, | |
| ) | |
| def test_eq(comparand: Any) -> None: | |
| subject = any.eq(comparand) | |
| assert subject == comparand | |
| assert subject != NON_SAMPLED | |
| @pytest.mark.parametrize( | |
| "comparand", | |
| SAMPLES, | |
| ) | |
| def test_ne(comparand: Any) -> None: | |
| subject = any.ne(comparand) | |
| assert subject == NON_SAMPLED | |
| @pytest.mark.parametrize( | |
| "comparand", | |
| SAMPLES, | |
| ) | |
| def test_of_type(comparand: Any) -> None: | |
| subject = any.of_type(type(comparand)) | |
| assert subject == comparand | |
| assert subject != NON_SAMPLED | |
| @pytest.mark.parametrize( | |
| "data, comparand, expected", | |
| [ | |
| pytest.param(["foo", "bar", "baz"], "foo", True, id="contains first"), | |
| pytest.param(["foo", "bar", "baz"], "bar", True, id="contains middle"), | |
| pytest.param(["foo", "bar", "baz"], "baz", True, id="contains last"), | |
| pytest.param(["foo", "bar", "baz"], "BAR", False, id="non-member"), | |
| pytest.param([], "foo", False, id="empty"), | |
| ], | |
| ) | |
| def test_in(data: list[str], comparand: Any, expected: bool) -> None: | |
| subject = any.in_(*data) | |
| actual = subject == comparand | |
| assert actual == expected | |
| @pytest.mark.parametrize( | |
| "n, comparand, expected", | |
| [ | |
| pytest.param(0, "", True, id="empty string"), | |
| pytest.param(6, "foobar", True, id="non-empty string"), | |
| pytest.param(3, ["foo", "bar", "baz"], True, id="list"), | |
| pytest.param(3, ("foo", "bar", "baz"), True, id="tuple"), | |
| pytest.param(3, {"foo": 1, "bar": 2, "baz": 3}, True, id="dict"), | |
| pytest.param(3, "foobar", False, id="string length mismatch"), | |
| pytest.param(3, [], False, id="list length mismatch"), | |
| pytest.param(0, 123, False, id="unsized"), | |
| ], | |
| ) | |
| def test_len(n: int, comparand: Any, expected: bool) -> None: | |
| subject = any.len(n) | |
| actual = subject == comparand | |
| assert actual == expected | |
| @pytest.mark.parametrize( | |
| "comparand, expected", | |
| [(1, False), (2, True), (3, False), (4, True), (5, False), (6, True), (7, False), (8, True), (9, False)], | |
| ) | |
| def test_where(comparand: int, expected: bool) -> None: | |
| subject = any.where(lambda x: x % 2 == 0) | |
| actual = subject == comparand | |
| assert actual == expected | |
| @pytest.mark.parametrize( | |
| "comparand, expected", | |
| [(1, False), (2, False), (3, False), (4, True), (5, False), (6, True), (7, False), (8, False), (9, False)], | |
| ) | |
| def test_where_combined(comparand: int, expected: bool) -> None: | |
| subject = any.where(lambda x: x % 2 == 0).where(lambda x: 3 <= x <= 7) | |
| actual = subject == comparand | |
| assert actual == expected | |
| def test_and() -> None: | |
| even = any.where(lambda x: x % 2 == 0) | |
| neg = any.where(lambda x: x < 0) | |
| subject = even.and_(neg) | |
| assert subject == -42 | |
| @pytest.mark.parametrize( | |
| "a, b, expected", [(False, False, False), (True, False, False), (False, True, False), (True, True, True)] | |
| ) | |
| def test_and_table(a: bool, b: bool, expected: bool) -> None: | |
| # arrange | |
| first = Mock() | |
| first.__eq__ = Mock(return_value=a) | |
| second = Mock() | |
| second.__eq__ = Mock(return_value=b) | |
| subject = any.and_(first).and_(second) | |
| # act | |
| actual = subject == Custom() | |
| # assert | |
| assert actual == expected | |
| def test_and_never_evaluates_second_when_first_is_unsatisfied() -> None: | |
| # arrange | |
| first = Mock() | |
| first.__eq__ = Mock(return_value=False) | |
| second = Mock() | |
| second.__eq__ = Mock(side_effect=AssertionError("Unexpected call")) | |
| subject = any.and_(first).and_(second) | |
| # act | |
| actual = subject == (other := Custom()) | |
| # assert | |
| assert not actual | |
| first.__eq__.assert_called_once_with(other) | |
| second.__eq__.assert_not_called() | |
| def test_and_evaluates_second_when_first_is_satisfied() -> None: | |
| # arrange | |
| first = Mock() | |
| first.__eq__ = Mock(return_value=True) | |
| second = Mock() | |
| second.__eq__ = Mock(return_value=True) | |
| subject = any.and_(first).and_(second) | |
| # act | |
| actual = subject == (other := Custom()) | |
| # assert | |
| assert actual | |
| first.__eq__.assert_called_once_with(other) | |
| second.__eq__.assert_called_once_with(other) | |
| def test_not() -> None: | |
| zero = any.eq(0) | |
| non_zero = zero.not_() | |
| even = any.where(lambda x: x % 2 == 0) | |
| odd = even.not_() | |
| neg = any.where(lambda x: x < 0) | |
| pos = neg.not_() | |
| assert non_zero == 1 | |
| assert odd == -3 | |
| assert pos == 42 | |
| assert odd.and_(pos) == 5 | |
| def test_not_not() -> None: | |
| zero = any.eq(0).not_().not_() | |
| assert zero == 0 | |
| assert zero != 42 | |
| assert zero != -42 | |
| def test_having() -> None: | |
| comparand = {"foo": 1, "bar": 2, "baz": 3, "456": [4, 5, 6]} | |
| def key(name: str) -> Callable[[dict[str, Any]], Any]: | |
| return lambda x: x[name] | |
| foo = key("foo") | |
| bar = key("bar") | |
| baz = key("baz") | |
| non_zero = any.ne(0) | |
| in_123 = any.in_(1, 2, 3) | |
| # The following set of assertions are really partial equality tests. They | |
| # test that the condition on the left is satisfied by the comparand (a | |
| # dictionary) on the right side of the `==` equal operator, such that the | |
| # following is saying that the dictionary is expected to have a key "foo" | |
| # with a value of 1. Other keys of the dictionary are ignored. | |
| assert any.having(foo, any.eq(1)) == comparand | |
| # The remaining assertions are in similar vein. | |
| assert any.having(bar, any.eq(2)) == comparand | |
| assert any.having(baz, any.eq(3)) == comparand | |
| assert any.having(foo, non_zero) == comparand | |
| assert any.having(bar, non_zero) == comparand | |
| assert any.having(baz, non_zero) == comparand | |
| assert any.having(foo, in_123) == comparand | |
| assert any.having(bar, in_123) == comparand | |
| assert any.having(baz, in_123) == comparand | |
| with pytest.raises(KeyError): | |
| assert any.having(key("qux"), any) == comparand | |
| def test_none() -> None: | |
| # | |
| # The following error is intentionally suppressed: | |
| # | |
| # > E711 Comparison to `None` should be `cond is None` | |
| # | |
| # because `__eq__` implementation is being abused for matching rather than | |
| # proper equality semantics. | |
| # | |
| assert any.none() == None # noqa: E711 | |
| @pytest.mark.parametrize( | |
| "comparand", | |
| SOME_SAMPLES, | |
| ) | |
| def test_none_inequality(comparand: Any) -> None: | |
| assert not (any.none() == comparand) | |
| @pytest.mark.parametrize( | |
| "comparand", | |
| SOME_SAMPLES, | |
| ) | |
| def test_not_none(comparand: Any) -> None: | |
| assert any.not_none() == comparand | |
| @pytest.mark.parametrize( | |
| "values, comparand, expected", | |
| [ | |
| pytest.param([1, 2, 3], [1, 2, 3], True, id="same"), | |
| pytest.param([1, 2, 3], [3, 2, 1], True, id="unordered"), | |
| pytest.param([1, 2, 3], [1, 1, 1], False, id="same length, different values"), | |
| pytest.param([Custom()], [Custom()], False, id="same length, different objects"), | |
| pytest.param([1, 2, 3], [], False, id="different lengths (empty)"), | |
| pytest.param([1, 2, 3], [1], False, id="different lengths"), | |
| pytest.param([], [], True, id="empty"), | |
| pytest.param([], 123, False, id="comparand non-iterable"), | |
| ], | |
| ) | |
| def test_count_equal(values: list[Any], comparand: Any, expected: bool) -> None: | |
| subject = any.count_equal(*values) | |
| actual = subject == comparand | |
| assert actual == expected | |
| @pytest.mark.parametrize( | |
| "comparand, expected", | |
| [ | |
| pytest.param([], False, id="empty"), | |
| pytest.param([42], True, id="not empty"), | |
| pytest.param(42, False, id="non-list"), | |
| ], | |
| ) | |
| def test_non_empty_list(comparand: Any, expected: bool) -> None: | |
| subject = any.non_empty_list() | |
| actual = subject == comparand | |
| assert actual == expected | |
| @pytest.mark.parametrize( | |
| "comparand, expected", | |
| [(0, False), (1, True), (2, True), (3, True), (4, False), (5, False), (6, False), (7, True), (8, True), (9, True)], | |
| ) | |
| def test_either_123_789(comparand: Any, expected: bool) -> None: | |
| one_two_three = any.in_(1, 2, 3) | |
| seven_eight_nine = any.in_(7, 8, 9) | |
| subject = either(one_two_three, seven_eight_nine) | |
| actual = subject == comparand | |
| assert actual == expected | |
| @pytest.mark.parametrize( | |
| "a, b, expected", [(False, False, False), (True, False, True), (False, True, True), (True, True, True)] | |
| ) | |
| def test_either_table(a: bool, b: bool, expected: bool) -> None: | |
| # arrange | |
| first = Mock() | |
| first.__eq__ = Mock(return_value=a) | |
| second = Mock() | |
| second.__eq__ = Mock(return_value=b) | |
| subject = either(first, second) | |
| # act | |
| actual = subject == Custom() | |
| # assert | |
| assert actual == expected | |
| def test_either_never_evaluates_second_when_first_is_satisfied() -> None: | |
| # arrange | |
| first = Mock() | |
| first.__eq__ = Mock(return_value=True) | |
| second = Mock() | |
| second.__eq__ = Mock(side_effect=AssertionError("Unexpected call")) | |
| subject = either(first, second) | |
| # act | |
| actual = subject == (other := Custom()) | |
| # assert | |
| assert actual | |
| first.__eq__.assert_called_once_with(other) | |
| second.__eq__.assert_not_called() | |
| def test_either_evaluates_second_when_first_is_unsatisfied() -> None: | |
| # arrange | |
| first = Mock() | |
| first.__eq__ = Mock(return_value=False) | |
| second = Mock() | |
| second.__eq__ = Mock(return_value=True) | |
| subject = either(first, second) | |
| # act | |
| actual = subject == (other := Custom()) | |
| # assert | |
| assert actual | |
| first.__eq__.assert_called_once_with(other) | |
| second.__eq__.assert_called_once_with(other) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment