Skip to content

Commit 7118afc

Browse files
committed
Use repr() to escape keys in MultiDict __repr__ output
Wrapping keys in literal single quotes produced unparseable output when keys contained a single quote (e.g. ``"<MultiDict('a'b': 1)>"``). Both the pure-Python and C-extension code paths now format keys via repr(), so single quotes and other special characters round-trip correctly.
1 parent 8d0edb4 commit 7118afc

4 files changed

Lines changed: 30 additions & 12 deletions

File tree

CHANGES/1342.bugfix.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Fixed ``__repr__`` of :class:`~multidict.MultiDict`,
2+
:class:`~multidict.CIMultiDict`, their proxies, and the keys/items views
3+
producing unparseable output when keys contained quote characters --
4+
keys are now formatted with :func:`repr` so the result is a valid Python
5+
string literal -- by :user:`aiolibsbot`.

multidict/_multidict_py.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ def _iter(self, version: int) -> Iterator[tuple[str, _V]]:
103103
def __repr__(self) -> str:
104104
lst = []
105105
for e in self._md._keys.iter_entries():
106-
lst.append(f"'{e.key}': {e.value!r}")
106+
lst.append(f"{e.key!r}: {e.value!r}")
107107
body = ", ".join(lst)
108108
return f"<{self.__class__.__name__}({body})>"
109109

@@ -300,7 +300,7 @@ def _iter(self, version: int) -> Iterator[str]:
300300
def __repr__(self) -> str:
301301
lst = []
302302
for e in self._md._keys.iter_entries():
303-
lst.append(f"'{e.key}'")
303+
lst.append(f"{e.key!r}")
304304
body = ", ".join(lst)
305305
return f"<{self.__class__.__name__}({body})>"
306306

@@ -753,7 +753,7 @@ def __contains__(self, key: object) -> bool:
753753

754754
@reprlib.recursive_repr()
755755
def __repr__(self) -> str:
756-
body = ", ".join(f"'{e.key}': {e.value!r}" for e in self._keys.iter_entries())
756+
body = ", ".join(f"{e.key!r}: {e.value!r}" for e in self._keys.iter_entries())
757757
return f"<{self.__class__.__name__}({body})>"
758758

759759
if sys.implementation.name != "pypy":
@@ -1207,7 +1207,7 @@ def __contains__(self, key: object) -> bool:
12071207

12081208
@reprlib.recursive_repr()
12091209
def __repr__(self) -> str:
1210-
body = ", ".join(f"'{k}': {v!r}" for k, v in self.items())
1210+
body = ", ".join(f"{k!r}: {v!r}" for k, v in self.items())
12111211
return f"<{self.__class__.__name__}({body})>"
12121212

12131213
def copy(self) -> MultiDict[_V]:

multidict/_multilib/hashtable.h

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1809,14 +1809,9 @@ md_repr(MultiDictObject *md, PyObject *name, bool show_keys, bool show_values)
18091809
}
18101810
}
18111811
if (show_keys) {
1812-
if (PyUnicodeWriter_WriteChar(writer, '\'') < 0) {
1813-
goto fail;
1814-
}
1815-
/* Don't need to convert key to istr, the text is the same*/
1816-
if (PyUnicodeWriter_WriteStr(writer, key) < 0) {
1817-
goto fail;
1818-
}
1819-
if (PyUnicodeWriter_WriteChar(writer, '\'') < 0) {
1812+
/* Use repr() so keys containing quotes produce parseable output.
1813+
*/
1814+
if (PyUnicodeWriter_WriteRepr(writer, key) < 0) {
18201815
goto fail;
18211816
}
18221817
}

tests/test_multidict.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -635,6 +635,24 @@ def test_repr_aiohttp_issue_410(self, cls: type[MutableMultiMapping[str]]) -> No
635635

636636
assert sys.exc_info()[1] == e # noqa: PT017
637637

638+
def test__repr__quotes_keys(self, cls: type[MultiDict[str]]) -> None:
639+
# Keys containing quotes must be repr'd as parseable Python literals,
640+
# not naively wrapped in single quotes.
641+
d = cls([("a'b", "v")])
642+
_cls = type(d)
643+
644+
# repr("a'b") == '"a\'b"' (Python uses double quotes when the string
645+
# contains a single quote and no double quote).
646+
assert str(d) == f"<{_cls.__name__}(\"a'b\": 'v')>"
647+
648+
def test_items__repr__quotes_keys(self, cls: type[MultiDict[str]]) -> None:
649+
d = cls([("a'b", "v")])
650+
assert repr(d.items()) == "<_ItemsView(\"a'b\": 'v')>"
651+
652+
def test_keys__repr__quotes_keys(self, cls: type[MultiDict[str]]) -> None:
653+
d = cls([("a'b", "v")])
654+
assert repr(d.keys()) == '<_KeysView("a\'b")>'
655+
638656
@pytest.mark.parametrize(
639657
"op",
640658
(operator.or_, operator.and_, operator.sub, operator.xor),

0 commit comments

Comments
 (0)