From 1106f899e4eed9686129b88c423835caea0710c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edgar=20Ram=C3=ADrez=20Mondrag=C3=B3n?= Date: Mon, 31 Aug 2026 10:39:56 -0600 Subject: [PATCH 1/4] Support packing Python 3.15 `frozentdict` (PEP 814) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Edgar Ramírez Mondragón --- msgpack/_packer.pyx | 15 ++++++++++++++- msgpack/fallback.py | 14 +++++++++++++- test/test_pack.py | 20 ++++++++++++++++++++ 3 files changed, 47 insertions(+), 2 deletions(-) diff --git a/msgpack/_packer.pyx b/msgpack/_packer.pyx index ca6cf983..3e1613cb 100644 --- a/msgpack/_packer.pyx +++ b/msgpack/_packer.pyx @@ -1,3 +1,5 @@ +import sys + from cpython cimport * from cpython.bytearray cimport PyByteArray_Check, PyByteArray_CheckExact from cpython.datetime cimport ( @@ -10,6 +12,16 @@ cdef Timestamp from .ext import ExtType, Timestamp +# frozendict is a builtin type added in Python 3.15 (PEP 814). +if sys.version_info >= (3, 15): + import builtins + + frozendict = builtins.frozendict +else: + + class frozendict: + pass + cdef extern from "Python.h": @@ -202,7 +214,8 @@ cdef class Packer: rawval = o msgpack_pack_raw(&self.pk, L) msgpack_pack_raw_body(&self.pk, rawval, L) - elif PyDict_CheckExact(o) if strict else PyDict_Check(o): + elif (PyDict_CheckExact(o) or type(o) is frozendict) if strict \ + else (PyDict_Check(o) or isinstance(o, frozendict)): L = len(o) if L > ITEM_LIMIT: raise ValueError("dict is too large") diff --git a/msgpack/fallback.py b/msgpack/fallback.py index e219786e..9cc0320e 100644 --- a/msgpack/fallback.py +++ b/msgpack/fallback.py @@ -37,6 +37,17 @@ def newlist_hint(size): return [] +# frozendict is a builtin type added in Python 3.15 (PEP 814). +if sys.version_info >= (3, 15): + import builtins + + frozendict = builtins.frozendict +else: + + class frozendict: + pass + + from .exceptions import BufferFull, ExtraData, FormatError, OutOfData, StackError from .ext import ExtType, Timestamp @@ -695,6 +706,7 @@ def _pack( list_types = list else: list_types = (list, tuple) + dict_types = (dict, frozendict) while True: if nest_limit < 0: raise ValueError("recursion limit exceeded") @@ -788,7 +800,7 @@ def _pack( for i in range(n): self._pack(obj[i], nest_limit - 1) return - if check(obj, dict): + if check(obj, dict_types): return self._pack_map_pairs(len(obj), obj.items(), nest_limit - 1) if self._datetime and check(obj, _DateTime) and obj.tzinfo is not None: diff --git a/test/test_pack.py b/test/test_pack.py index f44bd557..6d4e1d5c 100644 --- a/test/test_pack.py +++ b/test/test_pack.py @@ -1,6 +1,7 @@ #!/usr/bin/env python import struct +import sys from collections import OrderedDict from io import BytesIO @@ -162,6 +163,25 @@ def pair_hook(seq): assert unpackb(packb(od), object_pairs_hook=pair_hook, use_list=1) == seq +@pytest.mark.skipif(sys.version_info < (3, 15), reason="frozendict requires Python 3.15+") +def test_frozendict(): + import builtins + + fd = builtins.frozendict(a=1, b=[1, 2, 3]) + unpacked = unpackb(packb(fd), use_list=1) + assert isinstance(unpacked, dict) + assert unpacked == fd + + unpacked = unpackb(packb(fd), use_list=1, object_pairs_hook=builtins.frozendict) + assert isinstance(fd, builtins.frozendict) + assert unpacked == fd + + packer = Packer(strict_types=True) + unpacked = unpackb(packer.pack(fd), use_list=1) + assert isinstance(unpacked, dict) + assert unpacked == fd + + def test_pairlist(): pairlist = [(b"a", 1), (2, b"b"), (b"foo", b"bar")] packer = Packer() From 31508e223f3bb291bd37b4063616a62e755236cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edgar=20Ram=C3=ADrez=20Mondrag=C3=B3n?= <16805946+edgarrmondragon@users.noreply.github.com> Date: Tue, 1 Sep 2026 08:06:14 -0600 Subject: [PATCH 2/4] Fix isinstance assertion Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- test/test_pack.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_pack.py b/test/test_pack.py index 6d4e1d5c..9ca6e182 100644 --- a/test/test_pack.py +++ b/test/test_pack.py @@ -173,7 +173,7 @@ def test_frozendict(): assert unpacked == fd unpacked = unpackb(packb(fd), use_list=1, object_pairs_hook=builtins.frozendict) - assert isinstance(fd, builtins.frozendict) + assert isinstance(unpacked, builtins.frozendict) assert unpacked == fd packer = Packer(strict_types=True) From 0d3fce60568168fddbcd9dba50daed582f9b1608 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edgar=20Ram=C3=ADrez=20Mondrag=C3=B3n?= Date: Wed, 2 Sep 2026 09:22:12 -0600 Subject: [PATCH 3/4] Use `PyFrozenDict_Check` and `PyFrozenDict_CheckExact` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Found out these exist while reading through https://vstinner.github.io/pep-814-add-frozendict-builtin-type.html. https://github.com/python/cpython/blob/39a9a47516abf8a68cb3239f968c2b2135c3a5a7/Include/cpython/dictobject.h#L35-L43 Signed-off-by: Edgar Ramírez Mondragón --- msgpack/_packer.pyx | 21 +++++++-------------- msgpack/frozendict_compat.h | 15 +++++++++++++++ 2 files changed, 22 insertions(+), 14 deletions(-) create mode 100644 msgpack/frozendict_compat.h diff --git a/msgpack/_packer.pyx b/msgpack/_packer.pyx index 3e1613cb..d8f95c76 100644 --- a/msgpack/_packer.pyx +++ b/msgpack/_packer.pyx @@ -1,5 +1,3 @@ -import sys - from cpython cimport * from cpython.bytearray cimport PyByteArray_Check, PyByteArray_CheckExact from cpython.datetime cimport ( @@ -12,21 +10,16 @@ cdef Timestamp from .ext import ExtType, Timestamp -# frozendict is a builtin type added in Python 3.15 (PEP 814). -if sys.version_info >= (3, 15): - import builtins - - frozendict = builtins.frozendict -else: - - class frozendict: - pass - cdef extern from "Python.h": int PyMemoryView_Check(object obj) +cdef extern from "frozendict_compat.h": + + bint PyFrozenDict_Check(object obj) + bint PyFrozenDict_CheckExact(object obj) + cdef extern from "pack.h": struct msgpack_packer: char* buf @@ -214,8 +207,8 @@ cdef class Packer: rawval = o msgpack_pack_raw(&self.pk, L) msgpack_pack_raw_body(&self.pk, rawval, L) - elif (PyDict_CheckExact(o) or type(o) is frozendict) if strict \ - else (PyDict_Check(o) or isinstance(o, frozendict)): + elif (PyDict_CheckExact(o) or PyFrozenDict_CheckExact(o)) if strict \ + else (PyDict_Check(o) or PyFrozenDict_Check(o)): L = len(o) if L > ITEM_LIMIT: raise ValueError("dict is too large") diff --git a/msgpack/frozendict_compat.h b/msgpack/frozendict_compat.h new file mode 100644 index 00000000..874c7ff3 --- /dev/null +++ b/msgpack/frozendict_compat.h @@ -0,0 +1,15 @@ +#ifndef MSGPACK_FROZENDICT_COMPAT_H +#define MSGPACK_FROZENDICT_COMPAT_H + +#include + +/* frozendict (PEP 814) is a builtin type added in CPython 3.15. */ +#if PY_VERSION_HEX >= 0x030f0000 +/* PyFrozenDict_Check / PyFrozenDict_CheckExact come from + * Include/cpython/dictobject.h, pulled in transitively by Python.h. */ +#else +#define PyFrozenDict_Check(op) 0 +#define PyFrozenDict_CheckExact(op) 0 +#endif + +#endif From 220921e8e87e76bee438f40d41a2c8b494b004f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edgar=20Ram=C3=ADrez=20Mondrag=C3=B3n?= Date: Wed, 2 Sep 2026 10:09:29 -0600 Subject: [PATCH 4/4] Bump Cython to 3.3.0+ and use `PyAnyDict_Check` and `PyAnyDict_CheckExact` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Edgar Ramírez Mondragón --- msgpack/_packer.pyx | 9 ++------- msgpack/frozendict_compat.h | 15 --------------- pyproject.toml | 2 +- requirements.txt | 2 +- 4 files changed, 4 insertions(+), 24 deletions(-) delete mode 100644 msgpack/frozendict_compat.h diff --git a/msgpack/_packer.pyx b/msgpack/_packer.pyx index d8f95c76..52f4f1f2 100644 --- a/msgpack/_packer.pyx +++ b/msgpack/_packer.pyx @@ -4,6 +4,7 @@ from cpython.datetime cimport ( PyDateTime_CheckExact, PyDelta_CheckExact, datetime_tzinfo, timedelta_days, timedelta_seconds, timedelta_microseconds, ) +from cpython.frozendict cimport PyAnyDict_Check, PyAnyDict_CheckExact cdef ExtType cdef Timestamp @@ -15,11 +16,6 @@ cdef extern from "Python.h": int PyMemoryView_Check(object obj) -cdef extern from "frozendict_compat.h": - - bint PyFrozenDict_Check(object obj) - bint PyFrozenDict_CheckExact(object obj) - cdef extern from "pack.h": struct msgpack_packer: char* buf @@ -207,8 +203,7 @@ cdef class Packer: rawval = o msgpack_pack_raw(&self.pk, L) msgpack_pack_raw_body(&self.pk, rawval, L) - elif (PyDict_CheckExact(o) or PyFrozenDict_CheckExact(o)) if strict \ - else (PyDict_Check(o) or PyFrozenDict_Check(o)): + elif PyAnyDict_CheckExact(o) if strict else PyAnyDict_Check(o): L = len(o) if L > ITEM_LIMIT: raise ValueError("dict is too large") diff --git a/msgpack/frozendict_compat.h b/msgpack/frozendict_compat.h deleted file mode 100644 index 874c7ff3..00000000 --- a/msgpack/frozendict_compat.h +++ /dev/null @@ -1,15 +0,0 @@ -#ifndef MSGPACK_FROZENDICT_COMPAT_H -#define MSGPACK_FROZENDICT_COMPAT_H - -#include - -/* frozendict (PEP 814) is a builtin type added in CPython 3.15. */ -#if PY_VERSION_HEX >= 0x030f0000 -/* PyFrozenDict_Check / PyFrozenDict_CheckExact come from - * Include/cpython/dictobject.h, pulled in transitively by Python.h. */ -#else -#define PyFrozenDict_Check(op) 0 -#define PyFrozenDict_CheckExact(op) 0 -#endif - -#endif diff --git a/pyproject.toml b/pyproject.toml index ebc6b50d..aee0d5ed 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,6 +47,6 @@ lint.select = [ [dependency-groups] dev = [ - "cython>=3.2.5", + "cython>=3.3.0", "pytest>=9.0.3", ] diff --git a/requirements.txt b/requirements.txt index 7991e3f0..5e3122f6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -cython==3.2.5 +cython==3.3.0 setuptools==78.1.1 pytest build