From a88589beca9d2a6a41f7ca19696743b8245c26af Mon Sep 17 00:00:00 2001 From: Vizonex Date: Tue, 28 Apr 2026 15:29:02 -0500 Subject: [PATCH 1/8] fix __repr__ race (Free-Threaded) --- multidict/_multilib/hashtable.h | 2 ++ tests/isolated/multidict_repr_ft.py | 37 +++++++++++++++++++++++++++++ tests/test_leaks.py | 1 + 3 files changed, 40 insertions(+) create mode 100644 tests/isolated/multidict_repr_ft.py diff --git a/multidict/_multilib/hashtable.h b/multidict/_multilib/hashtable.h index f2ba9868a..4e76283e0 100644 --- a/multidict/_multilib/hashtable.h +++ b/multidict/_multilib/hashtable.h @@ -1769,6 +1769,7 @@ md_repr(MultiDictObject *md, PyObject *name, bool show_keys, bool show_values) PyObject *key = NULL; PyObject *value = NULL; + Py_BEGIN_CRITICAL_SECTION(md); bool comma = false; uint64_t version = md->version; @@ -1850,6 +1851,7 @@ md_repr(MultiDictObject *md, PyObject *name, bool show_keys, bool show_values) Py_CLEAR(key); Py_CLEAR(value); PyUnicodeWriter_Discard(writer); + Py_END_CRITICAL_SECTION(); return NULL; } diff --git a/tests/isolated/multidict_repr_ft.py b/tests/isolated/multidict_repr_ft.py new file mode 100644 index 000000000..b04b84311 --- /dev/null +++ b/tests/isolated/multidict_repr_ft.py @@ -0,0 +1,37 @@ +import os +import subprocess +import sys +import sysconfig +import textwrap + +FREETHREADED = bool(sysconfig.get_config_var("Py_GIL_DISABLED")) + + +if __name__ == "__main__" and FREETHREADED: + child = textwrap.dedent(""" + import signal, threading, time + signal.alarm(20) + from multidict import MultiDict + md = MultiDict([(f"k{i}", i) for i in range(50)]) + errors = [] + stop = threading.Event() + def repr_loop(): + while not stop.is_set(): + try: repr(md) + except Exception as e: errors.append(type(e).__name__) + def mutate_loop(): + i = 0 + while not stop.is_set(): md.add(f"m{i}", i); i += 1 + ts = [threading.Thread(target=repr_loop, daemon=True) for _ in range(2)] + ts += [threading.Thread(target=mutate_loop, daemon=True) for _ in range(2)] + for t in ts: t.start() + time.sleep(0.3); stop.set(); time.sleep(0.05) + print(f"repr errors: {len(errors)}") + """) + subprocess.run( + [sys.executable, "-c", child], + env={**os.environ, "PYTHON_GIL": "0"}, + capture_output=True, + timeout=60, + check=True, + ) diff --git a/tests/test_leaks.py b/tests/test_leaks.py index c39abe245..660fd0f43 100644 --- a/tests/test_leaks.py +++ b/tests/test_leaks.py @@ -11,6 +11,7 @@ @pytest.mark.parametrize( ("script"), ( + "multidict_repr_ft.py", "multidict_extend_dict.py", "multidict_extend_multidict.py", "multidict_extend_tuple.py", From 2f35d999393afb8a6aaddeeb4c071f50f6b3f048 Mon Sep 17 00:00:00 2001 From: Vizonex Date: Tue, 28 Apr 2026 15:35:18 -0500 Subject: [PATCH 2/8] add change to timeline --- CHANGES/1328.bugfix.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 CHANGES/1328.bugfix.rst diff --git a/CHANGES/1328.bugfix.rst b/CHANGES/1328.bugfix.rst new file mode 100644 index 000000000..8f7b4b77c --- /dev/null +++ b/CHANGES/1328.bugfix.rst @@ -0,0 +1,2 @@ +Fixed race when calling for a string representation of a ``MultiDict`` object. +-- by :user:`Vizonex` \ No newline at end of file From b545d5de959be3b4c4344b1c7a18ba29261e862a Mon Sep 17 00:00:00 2001 From: Vizonex <114684698+Vizonex@users.noreply.github.com> Date: Tue, 28 Apr 2026 15:54:03 -0500 Subject: [PATCH 3/8] Update CHANGES/1328.bugfix.rst MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 🇺🇦 Sviatoslav Sydorenko (Святослав Сидоренко) --- CHANGES/1328.bugfix.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES/1328.bugfix.rst b/CHANGES/1328.bugfix.rst index 8f7b4b77c..cffc8b5b5 100644 --- a/CHANGES/1328.bugfix.rst +++ b/CHANGES/1328.bugfix.rst @@ -1,2 +1,3 @@ Fixed race when calling for a string representation of a ``MultiDict`` object. + -- by :user:`Vizonex` \ No newline at end of file From 82558d3b7ec831d8f8d13582caff253259f44322 Mon Sep 17 00:00:00 2001 From: Vizonex Date: Tue, 28 Apr 2026 15:57:09 -0500 Subject: [PATCH 4/8] wrap MultiDict in CHANGES notes --- CHANGES/1328.bugfix.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES/1328.bugfix.rst b/CHANGES/1328.bugfix.rst index 8f7b4b77c..c56488e58 100644 --- a/CHANGES/1328.bugfix.rst +++ b/CHANGES/1328.bugfix.rst @@ -1,2 +1,2 @@ -Fixed race when calling for a string representation of a ``MultiDict`` object. +Fixed race when calling for a string representation of a :class:`MultiDict` object. -- by :user:`Vizonex` \ No newline at end of file From 907e6f4f64a5c8745216c0c2a2ce0738da7f3b84 Mon Sep 17 00:00:00 2001 From: Vizonex <114684698+Vizonex@users.noreply.github.com> Date: Wed, 29 Apr 2026 20:45:36 -0500 Subject: [PATCH 5/8] Update CHANGES/1328.bugfix.rst MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 🇺🇦 Sviatoslav Sydorenko (Святослав Сидоренко) --- CHANGES/1328.bugfix.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES/1328.bugfix.rst b/CHANGES/1328.bugfix.rst index c56488e58..cd1212346 100644 --- a/CHANGES/1328.bugfix.rst +++ b/CHANGES/1328.bugfix.rst @@ -1,2 +1,3 @@ Fixed race when calling for a string representation of a :class:`MultiDict` object. + -- by :user:`Vizonex` \ No newline at end of file From 451ec4709389342308bccca3dfca179c139490f0 Mon Sep 17 00:00:00 2001 From: Vizonex Date: Wed, 29 Apr 2026 20:52:53 -0500 Subject: [PATCH 6/8] wrap as `:py:class:` --- CHANGES/1328.bugfix.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES/1328.bugfix.rst b/CHANGES/1328.bugfix.rst index cd1212346..63bfa4c34 100644 --- a/CHANGES/1328.bugfix.rst +++ b/CHANGES/1328.bugfix.rst @@ -1,3 +1,3 @@ -Fixed race when calling for a string representation of a :class:`MultiDict` object. +Fixed race when calling for a string representation of a :py:class:`MultiDict` object. -- by :user:`Vizonex` \ No newline at end of file From c496c956bc049ef722cdd022e9da6835e662e234 Mon Sep 17 00:00:00 2001 From: Vizonex Date: Wed, 29 Apr 2026 20:59:35 -0500 Subject: [PATCH 7/8] fix timeline comment. --- CHANGES/1328.bugfix.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES/1328.bugfix.rst b/CHANGES/1328.bugfix.rst index 63bfa4c34..6b43e8ef9 100644 --- a/CHANGES/1328.bugfix.rst +++ b/CHANGES/1328.bugfix.rst @@ -1,3 +1,3 @@ -Fixed race when calling for a string representation of a :py:class:`MultiDict` object. +Fixed race when calling for a string representation of a :py:class:`multidict.MultiDict` object. -- by :user:`Vizonex` \ No newline at end of file From 090f394b71ab442f4d3274baeaf858fe460f9356 Mon Sep 17 00:00:00 2001 From: Vizonex <114684698+Vizonex@users.noreply.github.com> Date: Thu, 30 Apr 2026 10:01:56 -0500 Subject: [PATCH 8/8] Update CHANGES/1328.bugfix.rst MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 🇺🇦 Sviatoslav Sydorenko (Святослав Сидоренко) --- CHANGES/1328.bugfix.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES/1328.bugfix.rst b/CHANGES/1328.bugfix.rst index 6b43e8ef9..a08279333 100644 --- a/CHANGES/1328.bugfix.rst +++ b/CHANGES/1328.bugfix.rst @@ -1,3 +1,3 @@ -Fixed race when calling for a string representation of a :py:class:`multidict.MultiDict` object. +Fixed race when calling for a string representation of a :py:class:`~multidict.MultiDict` object. -- by :user:`Vizonex` \ No newline at end of file