From 8780c614affc6a9d3960b23d20d261036e5d9f44 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Wed, 7 Oct 2026 20:10:16 +0200 Subject: [PATCH] fix: honour a falsey key in all_unique all_unique applied the key with `map(key, iterable) if key else iterable`, so a falsey callable was silently ignored. it now checks `key is not None` like the other key functions, same fix as #1309. --- docs/versions.rst | 1 + more_itertools/more.py | 2 +- tests/test_more.py | 13 +++++++++++++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/docs/versions.rst b/docs/versions.rst index ecc3811..e1b2fdb 100644 --- a/docs/versions.rst +++ b/docs/versions.rst @@ -9,6 +9,7 @@ Unreleased ---------- * Changes to existing functions: + * :func:`all_unique` now honours a falsey *key* callable (such as an object whose ``__bool__`` returns ``False``) instead of silently ignoring it. * :func:`bucket` now honours a falsey *validator* callable (such as an empty ``set`` subclass that defines ``__call__``) instead of silently replacing it with the default that accepts every key. * :func:`ichunked` now matches :func:`chunked` for ``n = 0`` and negative *n*: it yields nothing for a zero chunk size (without consuming the source) and raises ``ValueError: n must be at least 0`` instead of leaking :func:`itertools.islice`'s error. * :func:`running_min`, :func:`running_max`, :func:`running_mean`, and :func:`running_statistics` now consistently validate *maxlen* as an integer and support the index protocol. diff --git a/more_itertools/more.py b/more_itertools/more.py index 9c56ab3..deb8c29 100644 --- a/more_itertools/more.py +++ b/more_itertools/more.py @@ -4279,7 +4279,7 @@ def all_unique(iterable, key=None): seenset_add = seenset.add seenlist = [] seenlist_add = seenlist.append - for element in map(key, iterable) if key else iterable: + for element in map(key, iterable) if key is not None else iterable: try: if element in seenset: return False diff --git a/tests/test_more.py b/tests/test_more.py index 9ecdd06..84d0822 100644 --- a/tests/test_more.py +++ b/tests/test_more.py @@ -5153,6 +5153,19 @@ class AllUniqueTests(TestCase): self.assertEqual(mi.all_unique(iterable, lambda x: x), True) self.assertEqual(mi.all_unique(iterable, str.lower), False) + def test_falsey_key_is_honoured(self): + # A falsey callable is still a valid key and must not be ignored + class CaseFold: + def __bool__(self): + return False + + def __call__(self, x): + return x.lower() + + key = CaseFold() + self.assertFalse(key) # Sanity: the key itself is falsey + self.assertEqual(mi.all_unique(['A', 'B', 'C', 'b'], key), False) + def test_infinite(self): self.assertEqual(mi.all_unique(mi.prepend(3, count())), False)