From bf3d3f2ba46b7a95c9c42814193ac60d1551561b Mon Sep 17 00:00:00 2001 From: Apoorv Darshan Date: Thu, 9 Jul 2026 22:02:02 +0530 Subject: [PATCH 1/2] Fix MultiDict.values()/items() to return proper dict views MultiDict.values() and items() returned one-shot generator expressions, so they had no len() and were exhausted after a single iteration. This is inconsistent with keys() (a real dict view) and with the class docstring's promise that MultiDict "behaves exactly like a normal dict". Return collections.abc.ValuesView(self) / ItemsView(self) instead. Because MultiDict is a MutableMapping whose __getitem__ already yields the newest value per key, these standard views deliver the correct values while adding len() support and re-iterability, matching real dict views. Fixes #1113. This contribution is dedicated to the public domain. ai-assisted-by: Claude Code (claude-opus-4-8, Anthropic) --- bottle.py | 6 +++--- test/test_mdict.py | 22 ++++++++++++++++++++++ 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/bottle.py b/bottle.py index 053f9b41a..6dde75afd 100755 --- a/bottle.py +++ b/bottle.py @@ -89,7 +89,7 @@ def _cli_patch(cli_args): # pragma: no coverage from urllib.parse import urljoin, SplitResult as UrlSplitResult from urllib.parse import urlencode, quote as urlquote, unquote as urlunquote from http.cookies import SimpleCookie, Morsel, CookieError -from collections.abc import MutableMapping as DictMixin +from collections.abc import MutableMapping as DictMixin, ValuesView, ItemsView from types import ModuleType as new_module import pickle from io import BytesIO @@ -2095,10 +2095,10 @@ def keys(self): return self.dict.keys() def values(self): - return (v[-1] for v in self.dict.values()) + return ValuesView(self) def items(self): - return ((k, v[-1]) for k, v in self.dict.items()) + return ItemsView(self) def allitems(self): return ((k, v) for k, vl in self.dict.items() for v in vl) diff --git a/test/test_mdict.py b/test/test_mdict.py index b82cef26c..a94d62b49 100755 --- a/test/test_mdict.py +++ b/test/test_mdict.py @@ -21,6 +21,28 @@ def test_isadict(self): self.assertEqual('cay' in d, 'cay' in m) self.assertRaises(KeyError, lambda: m['cay']) + def test_views_are_reiterable_and_sized(self): + """ keys(), values() and items() should return dict-like views that + support len() and can be iterated more than once (see issue #1113). """ + m = MultiDict(a=1, b=2, c=3) + + for name, view in (('keys', m.keys()), + ('values', m.values()), + ('items', m.items())): + # Views must support len(), just like real dict views do. + self.assertEqual(len(view), 3, "len() failed for %s()" % name) + # Views must be re-iterable (generators are exhausted after one pass). + first = list(view) + second = list(view) + self.assertEqual(first, second, + "%s() view is not re-iterable" % name) + self.assertEqual(len(first), 3) + + # Views must reflect the newest value for each key, like dict access. + m['a'] = 10 + self.assertEqual(sorted(m.values()), [2, 3, 10]) + self.assertEqual(dict(m.items()), {'a': 10, 'b': 2, 'c': 3}) + def test_ismulti(self): """ MultiDict has some special features """ m = MultiDict(a=5) From e35e90b1bcdd3ab8c170c4f834285b82bc1bd9da Mon Sep 17 00:00:00 2001 From: Apoorv Darshan Date: Fri, 10 Jul 2026 13:50:14 +0530 Subject: [PATCH 2/2] Use MutableMapping's built-in views This contribution is dedicated to the public domain. ai-assisted-by: Codex (GPT-5, OpenAI) --- bottle.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/bottle.py b/bottle.py index 6dde75afd..c8c3b2713 100755 --- a/bottle.py +++ b/bottle.py @@ -89,7 +89,7 @@ def _cli_patch(cli_args): # pragma: no coverage from urllib.parse import urljoin, SplitResult as UrlSplitResult from urllib.parse import urlencode, quote as urlquote, unquote as urlunquote from http.cookies import SimpleCookie, Morsel, CookieError -from collections.abc import MutableMapping as DictMixin, ValuesView, ItemsView +from collections.abc import MutableMapping as DictMixin from types import ModuleType as new_module import pickle from io import BytesIO @@ -2094,18 +2094,12 @@ def __setitem__(self, key, value): def keys(self): return self.dict.keys() - def values(self): - return ValuesView(self) - - def items(self): - return ItemsView(self) - def allitems(self): return ((k, v) for k, vl in self.dict.items() for v in vl) iterkeys = keys - itervalues = values - iteritems = items + itervalues = DictMixin.values + iteritems = DictMixin.items iterallitems = allitems def get(self, key, default=None, index=-1, type=None):