From 47f30df7a961ee8370f73d0341d850e32799cf39b20c4414044bc26a9b432ac8 Mon Sep 17 00:00:00 2001 From: Steve Kowalik Date: Fri, 7 Nov 2025 03:31:08 +0000 Subject: [PATCH] - Update to 0.19.0: * Improve JSON encoding performance by up to 40%. * Ensure tuple and frozenset default values are treated identically whether specified by value or default_factory. * Fix memory leak of match_args in StructConfig object. * Fix memory leak in Raw.copy(). * Update decode signatures for PEP 688. * Generate __replace__ method on Struct types, for use with copy.replace. * Fix incorrect decoding of certain > 64 bit integers. * Call __post_init__ when converting from an object to a Struct. * BREAKING: Expand buffer when encode_into is passed a buffer smaller than offset. * Support Raw objects as inputs to convert. * Error nicely when a dataclass type (instead of an instance) is passed to encode. * Drop support for Python 3.8. * Add support for Python 3.13. * Remove deprecated from_builtins. * Support encoding any Enum type whose .value is a supported type. * Don't fail eagerly when processing generic types with unsupported __parameters__. * Use eval_type_backport to backport type annotations to Python 3.9. - Drop patch python313.patch, included upstream. - Add patch support-python314.patch: * Support Python 3.14 annontation changes. OBS-URL: https://build.opensuse.org/package/show/devel:languages:python/python-msgspec?expand=0&rev=13 --- .gitattributes | 23 +++ .gitignore | 1 + msgspec-0.18.6.tar.gz | 3 + msgspec-0.19.0.tar.gz | 3 + python-msgspec.changes | 98 ++++++++++ python-msgspec.spec | 69 +++++++ python313.patch | 300 +++++++++++++++++++++++++++++++ support-python314.patch | 387 ++++++++++++++++++++++++++++++++++++++++ 8 files changed, 884 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 msgspec-0.18.6.tar.gz create mode 100644 msgspec-0.19.0.tar.gz create mode 100644 python-msgspec.changes create mode 100644 python-msgspec.spec create mode 100644 python313.patch create mode 100644 support-python314.patch diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..9b03811 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,23 @@ +## Default LFS +*.7z filter=lfs diff=lfs merge=lfs -text +*.bsp filter=lfs diff=lfs merge=lfs -text +*.bz2 filter=lfs diff=lfs merge=lfs -text +*.gem filter=lfs diff=lfs merge=lfs -text +*.gz filter=lfs diff=lfs merge=lfs -text +*.jar filter=lfs diff=lfs merge=lfs -text +*.lz filter=lfs diff=lfs merge=lfs -text +*.lzma filter=lfs diff=lfs merge=lfs -text +*.obscpio filter=lfs diff=lfs merge=lfs -text +*.oxt filter=lfs diff=lfs merge=lfs -text +*.pdf filter=lfs diff=lfs merge=lfs -text +*.png filter=lfs diff=lfs merge=lfs -text +*.rpm filter=lfs diff=lfs merge=lfs -text +*.tbz filter=lfs diff=lfs merge=lfs -text +*.tbz2 filter=lfs diff=lfs merge=lfs -text +*.tgz filter=lfs diff=lfs merge=lfs -text +*.ttf filter=lfs diff=lfs merge=lfs -text +*.txz filter=lfs diff=lfs merge=lfs -text +*.whl filter=lfs diff=lfs merge=lfs -text +*.xz filter=lfs diff=lfs merge=lfs -text +*.zip filter=lfs diff=lfs merge=lfs -text +*.zst filter=lfs diff=lfs merge=lfs -text diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..57affb6 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.osc diff --git a/msgspec-0.18.6.tar.gz b/msgspec-0.18.6.tar.gz new file mode 100644 index 0000000..2537743 --- /dev/null +++ b/msgspec-0.18.6.tar.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5bff8c3f9303eb589516e02de4141ce31be09a0fdf4980734b020b64c120295e +size 1311867 diff --git a/msgspec-0.19.0.tar.gz b/msgspec-0.19.0.tar.gz new file mode 100644 index 0000000..1a2589f --- /dev/null +++ b/msgspec-0.19.0.tar.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:33961077a37830c54fa3108bd226a9d7a09b91ff82ef7b976a371039b54b6bc7 +size 1313340 diff --git a/python-msgspec.changes b/python-msgspec.changes new file mode 100644 index 0000000..4c8d4ce --- /dev/null +++ b/python-msgspec.changes @@ -0,0 +1,98 @@ +------------------------------------------------------------------- +Fri Nov 7 03:30:11 UTC 2025 - Steve Kowalik + +- Update to 0.19.0: + * Improve JSON encoding performance by up to 40%. + * Ensure tuple and frozenset default values are treated identically whether + specified by value or default_factory. + * Fix memory leak of match_args in StructConfig object. + * Fix memory leak in Raw.copy(). + * Update decode signatures for PEP 688. + * Generate __replace__ method on Struct types, for use with copy.replace. + * Fix incorrect decoding of certain > 64 bit integers. + * Call __post_init__ when converting from an object to a Struct. + * BREAKING: Expand buffer when encode_into is passed a buffer smaller than + offset. + * Support Raw objects as inputs to convert. + * Error nicely when a dataclass type (instead of an instance) is passed to + encode. + * Drop support for Python 3.8. + * Add support for Python 3.13. + * Remove deprecated from_builtins. + * Support encoding any Enum type whose .value is a supported type. + * Don't fail eagerly when processing generic types with unsupported + __parameters__. + * Use eval_type_backport to backport type annotations to Python 3.9. +- Drop patch python313.patch, included upstream. +- Add patch support-python314.patch: + * Support Python 3.14 annontation changes. + +------------------------------------------------------------------- +Wed Sep 17 13:09:18 UTC 2025 - ecsos + +- Add %{?sle15_python_module_pythons} + +------------------------------------------------------------------- +Mon Oct 28 22:29:06 UTC 2024 - Dirk Müller + +- add python313.patch: support python 3.13 + +------------------------------------------------------------------- +Tue Jan 30 18:06:33 UTC 2024 - Dirk Müller + +- update to 0.18.6: + * Support coercing integral floats to ints when strict=False + * Preserve leading _ when renaming fields to camel or pascal + case + * Support zero-copy decoding binary fields to a memoryview + * Fix a bug when inheriting from the same Generic base class + multiple times + * Add an order option to all encoders for enforcing + deterministic/sorted ordering when encoding. This can help + provide a more consistent or human readable output + * Support inheriting from any slots-class when defining a new + Struct type with gc=False + * Automatically infer the input field naming convention when + converting non-dict mappings or arbitrary objects to Struct + types in msgspec.convert + +------------------------------------------------------------------- +Wed Dec 27 10:51:29 UTC 2023 - Dirk Müller + +- update to 0.18.5: + * Support unhashable ``Annotated`` metadata in + `msgspec.inspect.type_info` + * Fix bug preventing decoding dataclasses/attrs types with + default values and ``slots=True, frozen=True`` (:pr:`569`). + * Support passing parametrized generic struct types to + `msgspec.structs.fields` + * Validate ``str`` constraints on dict keys when decoding + msgpack (:pr:`577`). + * Support ``UUID`` subclasses as inputs to `msgspec.convert` + * Call ``__eq__`` from generated ``__ne__`` if user defines + manual ``__eq__`` method on a ``Struct`` type (:pr:`593`). + * Include the ``Struct`` type in the generated hash + * Add a ``cache_hash`` struct option (:pr:`596`). + * Fix a bug around caching of dataclass type info when dealing + with subclasses of dataclasses (:pr:`599`). + * Add `msgspec.structs.force_setattr` (:pr:`600`). + * Support custom dict key types in JSON encoder and decoder + * Include ``dict`` key constraints in generated JSON schema via + the ``propertyNames`` field (:pr:`604`). + * Add a ``schema_hook`` for generating JSON schemas for custom + types + * Add support for Python 3.12's ``type`` aliases (:pr:`606`). + +------------------------------------------------------------------- +Thu Dec 7 23:03:38 UTC 2023 - Dirk Müller + +- update to 0.18.4: + * Resolve an issue leading to periodic segfaults when importing + ``msgspec`` on CPython 3.12 + * Improve type annotation for ``Struct.__rich_repr__`` + * Add pre-built wheels for Python 3.12 (:pr:`558`) + +------------------------------------------------------------------- +Fri Sep 1 07:09:26 UTC 2023 - Steve Kowalik + +- Initial release of 0.18.2. diff --git a/python-msgspec.spec b/python-msgspec.spec new file mode 100644 index 0000000..5ca1b40 --- /dev/null +++ b/python-msgspec.spec @@ -0,0 +1,69 @@ +# +# spec file for package python-msgspec +# +# Copyright (c) 2025 SUSE LLC and contributors +# +# All modifications and additions to the file contributed by third parties +# remain the property of their copyright owners, unless otherwise agreed +# upon. The license for this file, and modifications and additions to the +# file, is the same license as for the pristine package itself (unless the +# license for the pristine package is not an Open Source License, in which +# case the license is the MIT License). An "Open Source License" is a +# license that conforms to the Open Source Definition (Version 1.9) +# published by the Open Source Initiative. + +# Please submit bugfixes or comments via https://bugs.opensuse.org/ +# + + +%{?sle15_python_module_pythons} +Name: python-msgspec +Version: 0.19.0 +Release: 0 +Summary: A fast serialization and validation library +License: BSD-3-Clause +URL: https://jcristharif.com/msgspec/ +Source: https://github.com/jcrist/msgspec/archive/refs/tags/%{version}.tar.gz#/msgspec-%{version}.tar.gz +# PATCH-FIX-UPSTREAM Based on gh#jcrist/msgspec#852 & gh#jcrist/msgspec#854 +Patch0: support-python314.patch +BuildRequires: %{python_module devel >= 3.9} +BuildRequires: %{python_module pip} +BuildRequires: %{python_module pytest} +BuildRequires: %{python_module setuptools} +BuildRequires: %{python_module wheel} +BuildRequires: fdupes +BuildRequires: python-rpm-macros +Suggests: python-msgpack +Suggests: python-attrs +Suggests: python-furo +Suggests: python-ipython +Suggests: python-tomli_w +Suggests: python-pyyaml +%python_subpackages + +%description +A fast serialization and validation library, with builtin support for JSON, MessagePack, YAML, and TOML. + +%prep +%autosetup -p1 -n msgspec-%{version} + +%build +export CFLAGS="%{optflags}" +%pyproject_wheel + +%install +%pyproject_install +%python_expand %fdupes %{buildroot}%{$python_sitearch} + +%check +# Requires to import the module, but other tests require the path +# Can be dropped next release +%pytest_arch -k 'not test_raw_copy_doesnt_leak' + +%files %{python_files} +%doc README.md +%license LICENSE +%{python_sitearch}/msgspec +%{python_sitearch}/msgspec-%{version}.dist-info + +%changelog diff --git a/python313.patch b/python313.patch new file mode 100644 index 0000000..9b9020f --- /dev/null +++ b/python313.patch @@ -0,0 +1,300 @@ +From 7ade46952adea22f3b2bb9c2b8b3139e4f2831b7 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: Sun, 13 Oct 2024 13:25:41 -0600 +Subject: [PATCH] Support Python 3.13 (not free-threaded) (#711) + +* Build Python 3.13 wheels (not free-threaded) + +* Include cp313 wheels for testing + +* Build Python 3.13 wheels only for testing + +* Remove trove classifier + +* Update for Python 3.13.0rc1 + +* Clean up + +* Bump cibuildwheel to 2.21.1 + +* Upgrade deprecated artifact actions + +* Update for Python 3.13.0rc3 + +* Bump cibuildwheel to 2.21.3 to use Python 3.13.0 final + +* Squash _eval_type warning + +* Still use _PyUnicode_EQ for older pythons + +Keeps around the semantic meaning for when this function is added back +in Python 3.14. + +* Use `PyObject_GetIter` for all versions + +* Use PyLong_AsNativeBytes on Py3.13 + +* Suppress hashing error when parsing annotated types + +--------- + +Co-authored-by: Jim Crist-Harif +--- + .github/workflows/ci.yml | 11 +++++--- + msgspec/_core.c | 56 ++++++++++++++++++++++++++++++---------- + msgspec/_utils.py | 11 +++++++- + setup.cfg | 2 ++ + 4 files changed, 61 insertions(+), 19 deletions(-) + +Index: msgspec-0.18.6/.github/workflows/ci.yml +=================================================================== +--- msgspec-0.18.6.orig/.github/workflows/ci.yml ++++ msgspec-0.18.6/.github/workflows/ci.yml +@@ -80,7 +80,7 @@ jobs: + env: + CIBW_TEST_REQUIRES: "pytest msgpack pyyaml tomli tomli_w" + CIBW_TEST_COMMAND: "pytest {project}/tests" +- CIBW_BUILD: "cp38-* cp39-* cp310-* cp311-* cp312-*" ++ CIBW_BUILD: "cp38-* cp39-* cp310-* cp311-* cp312-* cp313-*" + CIBW_SKIP: "*-win32 *_i686 *_s390x *_ppc64le" + CIBW_ARCHS_MACOS: "x86_64 arm64" + CIBW_ARCHS_LINUX: "x86_64 aarch64" +@@ -129,6 +129,7 @@ jobs: + - name: Upload artifact + uses: actions/upload-artifact@v2 + with: ++ name: artifact-sdist + path: dist/*.tar.gz + + upload_pypi: +@@ -138,8 +139,9 @@ jobs: + steps: + - uses: actions/download-artifact@v2 + with: +- name: artifact ++ merge-multiple: true + path: dist ++ pattern: artifact-* + + - uses: pypa/gh-action-pypi-publish@master + with: +Index: msgspec-0.18.6/msgspec/_core.c +=================================================================== +--- msgspec-0.18.6.orig/msgspec/_core.c ++++ msgspec-0.18.6/msgspec/_core.c +@@ -20,6 +20,7 @@ + #define PY310_PLUS (PY_VERSION_HEX >= 0x030a0000) + #define PY311_PLUS (PY_VERSION_HEX >= 0x030b0000) + #define PY312_PLUS (PY_VERSION_HEX >= 0x030c0000) ++#define PY313_PLUS (PY_VERSION_HEX >= 0x030d0000) + + /* Hint to the compiler not to store `x` in a register since it is likely to + * change. Results in much higher performance on GCC, with smaller benefits on +@@ -56,6 +57,12 @@ ms_popcount(uint64_t i) { + #define SET_SIZE(obj, size) (((PyVarObject *)obj)->ob_size = size) + #endif + ++#if PY313_PLUS ++#define MS_UNICODE_EQ(a, b) (PyUnicode_Compare(a, b) == 0) ++#else ++#define MS_UNICODE_EQ(a, b) _PyUnicode_EQ(a, b) ++#endif ++ + #define DIV_ROUND_CLOSEST(n, d) ((((n) < 0) == ((d) < 0)) ? (((n) + (d)/2)/(d)) : (((n) - (d)/2)/(d))) + + /* These macros are used to manually unroll some loops */ +@@ -497,7 +504,7 @@ find_keyword(PyObject *kwnames, PyObject + for (i = 0; i < nkwargs; i++) { + PyObject *kwname = PyTuple_GET_ITEM(kwnames, i); + assert(PyUnicode_Check(kwname)); +- if (_PyUnicode_EQ(kwname, key)) { ++ if (MS_UNICODE_EQ(kwname, key)) { + return kwstack[i]; + } + } +@@ -4438,10 +4445,8 @@ typenode_collect_convert_structs(TypeNod + * + * If any of these checks fails, an appropriate error is returned. + */ +- PyObject *tag_mapping = NULL, *tag_field = NULL, *set_item = NULL; ++ PyObject *tag_mapping = NULL, *tag_field = NULL, *set_iter = NULL, *set_item = NULL; + PyObject *struct_info = NULL; +- Py_ssize_t set_pos = 0; +- Py_hash_t set_hash; + bool array_like = false; + bool tags_are_strings = true; + int status = -1; +@@ -4449,7 +4454,8 @@ typenode_collect_convert_structs(TypeNod + tag_mapping = PyDict_New(); + if (tag_mapping == NULL) goto cleanup; + +- while (_PySet_NextEntry(state->structs_set, &set_pos, &set_item, &set_hash)) { ++ set_iter = PyObject_GetIter(state->structs_set); ++ while ((set_item = PyIter_Next(set_iter))) { + struct_info = StructInfo_Convert(set_item); + if (struct_info == NULL) goto cleanup; + +@@ -4557,6 +4563,7 @@ typenode_collect_convert_structs(TypeNod + status = 0; + + cleanup: ++ Py_XDECREF(set_iter); + Py_XDECREF(tag_mapping); + Py_XDECREF(struct_info); + return status; +@@ -4612,11 +4619,15 @@ typenode_origin_args_metadata( + * abstract -> concrete mapping. If present, this is an unparametrized + * collection of some form. This helps avoid compatibility issues in + * Python 3.8, where unparametrized collections still have __args__. */ +- origin = PyDict_GetItem(state->mod->concrete_types, t); ++ origin = PyDict_GetItemWithError(state->mod->concrete_types, t); + if (origin != NULL) { + Py_INCREF(origin); + break; + } ++ else { ++ /* Ignore all errors in this initial check */ ++ PyErr_Clear(); ++ } + + /* If `t` is a type instance, no need to inspect further */ + if (PyType_CheckExact(t)) { +@@ -7313,7 +7324,7 @@ Struct_vectorcall(PyTypeObject *cls, PyO + * check for parameters passed both as arg and kwarg */ + for (field_index = 0; field_index < nfields; field_index++) { + PyObject *field = PyTuple_GET_ITEM(fields, field_index); +- if (_PyUnicode_EQ(kwname, field)) { ++ if (MS_UNICODE_EQ(kwname, field)) { + if (MS_UNLIKELY(field_index < nargs)) { + PyErr_Format( + PyExc_TypeError, +@@ -7720,7 +7731,7 @@ struct_replace(PyObject *self, PyObject + } + for (field_index = 0; field_index < nfields; field_index++) { + PyObject *field = PyTuple_GET_ITEM(fields, field_index); +- if (_PyUnicode_EQ(kwname, field)) goto kw_found; ++ if (MS_UNICODE_EQ(kwname, field)) goto kw_found; + } + + /* Unknown keyword */ +@@ -11251,7 +11262,16 @@ ms_uuid_to_16_bytes(MsgspecState *mod, P + PyErr_SetString(PyExc_TypeError, "uuid.int must be an int"); + return -1; + } ++#if PY313_PLUS ++ int out = (int)PyLong_AsNativeBytes( ++ int128, ++ buf, ++ 16, ++ Py_ASNATIVEBYTES_BIG_ENDIAN | Py_ASNATIVEBYTES_UNSIGNED_BUFFER ++ ); ++#else + int out = _PyLong_AsByteArray((PyLongObject *)int128, buf, 16, 0, 0); ++#endif + Py_DECREF(int128); + return out; + } +@@ -12403,8 +12423,7 @@ mpack_encode_list(EncoderState *self, Py + static int + mpack_encode_set(EncoderState *self, PyObject *obj) + { +- Py_ssize_t len, ppos = 0; +- Py_hash_t hash; ++ Py_ssize_t len = 0; + PyObject *item; + int status = -1; + +@@ -12423,13 +12442,18 @@ mpack_encode_set(EncoderState *self, PyO + + if (mpack_encode_array_header(self, len, "set") < 0) return -1; + if (Py_EnterRecursiveCall(" while serializing an object")) return -1; +- while (_PySet_NextEntry(obj, &ppos, &item, &hash)) { ++ ++ PyObject *iter = PyObject_GetIter(obj); ++ if (iter == NULL) goto cleanup; ++ ++ while ((item = PyIter_Next(iter))) { + if (mpack_encode_inline(self, item) < 0) goto cleanup; + } + status = 0; + + cleanup: + Py_LeaveRecursiveCall(); ++ Py_XDECREF(iter); + return status; + } + +@@ -13725,8 +13749,7 @@ json_encode_tuple(EncoderState *self, Py + static int + json_encode_set(EncoderState *self, PyObject *obj) + { +- Py_ssize_t len, ppos = 0; +- Py_hash_t hash; ++ Py_ssize_t len = 0; + PyObject *item; + int status = -1; + +@@ -13745,7 +13768,11 @@ json_encode_set(EncoderState *self, PyOb + + if (ms_write(self, "[", 1) < 0) return -1; + if (Py_EnterRecursiveCall(" while serializing an object")) return -1; +- while (_PySet_NextEntry(obj, &ppos, &item, &hash)) { ++ ++ PyObject *iter = PyObject_GetIter(obj); ++ if (iter == NULL) goto cleanup; ++ ++ while ((item = PyIter_Next(iter))) { + if (json_encode_inline(self, item) < 0) goto cleanup; + if (ms_write(self, ",", 1) < 0) goto cleanup; + } +@@ -13754,6 +13781,7 @@ json_encode_set(EncoderState *self, PyOb + status = 0; + cleanup: + Py_LeaveRecursiveCall(); ++ Py_XDECREF(iter); + return status; + } + +Index: msgspec-0.18.6/msgspec/_utils.py +=================================================================== +--- msgspec-0.18.6.orig/msgspec/_utils.py ++++ msgspec-0.18.6/msgspec/_utils.py +@@ -51,6 +51,15 @@ else: + return typing.ForwardRef(value, is_argument=False, is_class=True) + + ++# Python 3.13 adds a new mandatory type_params kwarg to _eval_type ++if sys.version_info >= (3, 13): ++ ++ def _eval_type(t, globalns, localns): ++ return typing._eval_type(t, globalns, localns, ()) ++else: ++ _eval_type = typing._eval_type ++ ++ + def _apply_params(obj, mapping): + if params := getattr(obj, "__parameters__", None): + args = tuple(mapping.get(p, p) for p in params) +@@ -127,7 +136,7 @@ def get_class_annotations(obj): + value = type(None) + elif isinstance(value, str): + value = _forward_ref(value) +- value = typing._eval_type(value, cls_locals, cls_globals) ++ value = _eval_type(value, cls_locals, cls_globals) + if mapping is not None: + value = _apply_params(value, mapping) + hints[name] = value +Index: msgspec-0.18.6/setup.cfg +=================================================================== +--- msgspec-0.18.6.orig/setup.cfg ++++ msgspec-0.18.6/setup.cfg +@@ -12,6 +12,8 @@ omit = + markers = + mypy + pyright ++filterwarnings = ++ error + + [versioneer] + VCS = git diff --git a/support-python314.patch b/support-python314.patch new file mode 100644 index 0000000..5858bf8 --- /dev/null +++ b/support-python314.patch @@ -0,0 +1,387 @@ +Index: msgspec-0.19.0/msgspec/_core.c +=================================================================== +--- msgspec-0.19.0.orig/msgspec/_core.c ++++ msgspec-0.19.0/msgspec/_core.c +@@ -452,6 +452,7 @@ typedef struct { + #endif + PyObject *astimezone; + PyObject *re_compile; ++ PyObject *get_annotate_from_class_namespace; + uint8_t gc_cycle; + } MsgspecState; + +@@ -5814,12 +5815,45 @@ structmeta_is_classvar( + + static int + structmeta_collect_fields(StructMetaInfo *info, MsgspecState *mod, bool kwonly) { +- PyObject *annotations = PyDict_GetItemString( ++ PyObject *annotations = PyDict_GetItemString( // borrowed reference + info->namespace, "__annotations__" + ); +- if (annotations == NULL) return 0; ++ if (annotations == NULL) { ++ if (mod->get_annotate_from_class_namespace != NULL) { ++ PyObject *annotate = PyObject_CallOneArg( ++ mod->get_annotate_from_class_namespace, info->namespace ++ ); ++ if (annotate == NULL) { ++ return -1; ++ } ++ if (annotate == Py_None) { ++ Py_DECREF(annotate); ++ return 0; ++ } ++ PyObject *format = PyLong_FromLong(1); /* annotationlib.Format.VALUE */ ++ if (format == NULL) { ++ Py_DECREF(annotate); ++ return -1; ++ } ++ annotations = PyObject_CallOneArg( ++ annotate, format ++ ); ++ Py_DECREF(annotate); ++ Py_DECREF(format); ++ if (annotations == NULL) { ++ return -1; ++ } ++ } ++ else { ++ return 0; // No annotations, nothing to do ++ } ++ } ++ else { ++ Py_INCREF(annotations); ++ } + + if (!PyDict_Check(annotations)) { ++ Py_DECREF(annotations); + PyErr_SetString(PyExc_TypeError, "__annotations__ must be a dict"); + return -1; + } +@@ -5869,6 +5903,7 @@ structmeta_collect_fields(StructMetaInfo + } + return 0; + error: ++ Py_DECREF(annotations); + Py_XDECREF(module_ns); + return -1; + } +@@ -22223,6 +22258,26 @@ PyInit__core(void) + Py_DECREF(temp_module); + if (st->re_compile == NULL) return NULL; + ++ /* annotationlib.get_annotate_from_class_namespace */ ++ temp_module = PyImport_ImportModule("annotationlib"); ++ if (temp_module == NULL) { ++ if (PyErr_ExceptionMatches(PyExc_ModuleNotFoundError)) { ++ // Below Python 3.14 ++ PyErr_Clear(); ++ st->get_annotate_from_class_namespace = NULL; ++ } ++ else { ++ return NULL; ++ } ++ } ++ else { ++ st->get_annotate_from_class_namespace = PyObject_GetAttrString( ++ temp_module, "get_annotate_from_class_namespace" ++ ); ++ Py_DECREF(temp_module); ++ if (st->get_annotate_from_class_namespace == NULL) return NULL; ++ } ++ + /* Initialize cached constant strings */ + #define CACHED_STRING(attr, str) \ + if ((st->attr = PyUnicode_InternFromString(str)) == NULL) return NULL +Index: msgspec-0.19.0/msgspec/_utils.py +=================================================================== +--- msgspec-0.19.0.orig/msgspec/_utils.py ++++ msgspec-0.19.0/msgspec/_utils.py +@@ -1,5 +1,6 @@ + # type: ignore + import collections ++import inspect + import sys + import typing + +@@ -71,6 +72,13 @@ else: + _eval_type = typing._eval_type + + ++if sys.version_info >= (3, 10): ++ from inspect import get_annotations as _get_class_annotations ++else: ++ def _get_class_annotations(cls): ++ return cls.__dict__.get("__annotations__", {}) ++ ++ + def _apply_params(obj, mapping): + if isinstance(obj, typing.TypeVar): + return mapping.get(obj, obj) +@@ -149,17 +157,17 @@ def get_class_annotations(obj): + cls_locals = dict(vars(cls)) + cls_globals = getattr(sys.modules.get(cls.__module__, None), "__dict__", {}) + +- ann = cls.__dict__.get("__annotations__", {}) ++ ann = _get_class_annotations(cls) + for name, value in ann.items(): + if name in hints: + continue +- if value is None: +- value = type(None) +- elif isinstance(value, str): ++ if isinstance(value, str): + value = _forward_ref(value) + value = _eval_type(value, cls_locals, cls_globals) + if mapping is not None: + value = _apply_params(value, mapping) ++ if value is None: ++ value = type(None) + hints[name] = value + return hints + +Index: msgspec-0.19.0/tests/test_common.py +=================================================================== +--- msgspec-0.19.0.orig/tests/test_common.py ++++ msgspec-0.19.0/tests/test_common.py +@@ -1370,14 +1370,14 @@ class TestGenericStruct: + dec = proto.Decoder(typ) + info = typ.__msgspec_cache__ + assert info is not None +- assert sys.getrefcount(info) == 4 # info + attr + decoder + func call ++ assert sys.getrefcount(info) <= 4 # info + attr + decoder + func call + dec2 = proto.Decoder(typ) + assert typ.__msgspec_cache__ is info +- assert sys.getrefcount(info) == 5 ++ assert sys.getrefcount(info) <= 5 + + del dec + del dec2 +- assert sys.getrefcount(info) == 3 ++ assert sys.getrefcount(info) <= 3 + + def test_generic_struct_invalid_types_not_cached(self, proto): + class Ex(Struct, Generic[T]): +@@ -1545,7 +1545,7 @@ class TestStructPostInit: + res = proto.decode(buf, type=typ) + assert res == msg + assert count == 2 # 1 for Ex(), 1 for decode +- assert sys.getrefcount(singleton) == 2 # 1 for ref, 1 for call ++ assert sys.getrefcount(singleton) <= 2 # 1 for ref, 1 for call + + @pytest.mark.parametrize("array_like", [False, True]) + @pytest.mark.parametrize("union", [False, True]) +@@ -1606,14 +1606,14 @@ class TestGenericDataclassOrAttrs: + dec = proto.Decoder(typ) + info = typ.__msgspec_cache__ + assert info is not None +- assert sys.getrefcount(info) == 4 # info + attr + decoder + func call ++ assert sys.getrefcount(info) <= 4 # info + attr + decoder + func call + dec2 = proto.Decoder(typ) + assert typ.__msgspec_cache__ is info +- assert sys.getrefcount(info) == 5 ++ assert sys.getrefcount(info) <= 5 + + del dec + del dec2 +- assert sys.getrefcount(info) == 3 ++ assert sys.getrefcount(info) <= 3 + + def test_generic_invalid_types_not_cached(self, decorator, proto): + @decorator +@@ -2179,14 +2179,14 @@ class TestTypedDict: + dec = proto.Decoder(typ) + info = typ.__msgspec_cache__ + assert info is not None +- assert sys.getrefcount(info) == 4 # info + attr + decoder + func call ++ assert sys.getrefcount(info) <= 4 # info + attr + decoder + func call + dec2 = proto.Decoder(typ) + assert typ.__msgspec_cache__ is info +- assert sys.getrefcount(info) == 5 ++ assert sys.getrefcount(info) <= 5 + + del dec + del dec2 +- assert sys.getrefcount(info) == 3 ++ assert sys.getrefcount(info) <= 3 + + def test_generic_typeddict_invalid_types_not_cached(self, proto): + TypedDict = pytest.importorskip("typing_extensions").TypedDict +@@ -2398,14 +2398,14 @@ class TestNamedTuple: + dec = proto.Decoder(typ) + info = typ.__msgspec_cache__ + assert info is not None +- assert sys.getrefcount(info) == 4 # info + attr + decoder + func call ++ assert sys.getrefcount(info) <= 4 # info + attr + decoder + func call + dec2 = proto.Decoder(typ) + assert typ.__msgspec_cache__ is info +- assert sys.getrefcount(info) == 5 ++ assert sys.getrefcount(info) <= 5 + + del dec + del dec2 +- assert sys.getrefcount(info) == 3 ++ assert sys.getrefcount(info) <= 3 + + def test_generic_namedtuple_invalid_types_not_cached(self, proto): + NamedTuple = pytest.importorskip("typing_extensions").NamedTuple +Index: msgspec-0.19.0/tests/test_convert.py +=================================================================== +--- msgspec-0.19.0.orig/tests/test_convert.py ++++ msgspec-0.19.0/tests/test_convert.py +@@ -220,7 +220,7 @@ class TestConvert: + x = Custom() + res = convert(x, Any) + assert res is x +- assert sys.getrefcount(x) == 3 # x + res + 1 ++ assert sys.getrefcount(x) <= 3 # x + res + 1 + + def test_custom_input_type_works_with_custom(self): + class Custom: +@@ -229,7 +229,7 @@ class TestConvert: + x = Custom() + res = convert(x, Custom) + assert res is x +- assert sys.getrefcount(x) == 3 # x + res + 1 ++ assert sys.getrefcount(x) <= 3 # x + res + 1 + + def test_custom_input_type_works_with_dec_hook(self): + class Custom: +@@ -247,8 +247,8 @@ class TestConvert: + x = Custom() + res = convert(x, Custom2, dec_hook=dec_hook) + assert isinstance(res, Custom2) +- assert sys.getrefcount(res) == 2 # res + 1 +- assert sys.getrefcount(x) == 2 # x + 1 ++ assert sys.getrefcount(res) <= 2 # res + 1 ++ assert sys.getrefcount(x) <= 2 # x + 1 + + def test_unsupported_output_type(self): + with pytest.raises(TypeError, match="more than one array-like"): +@@ -397,7 +397,7 @@ class TestInt: + x = MyInt(100) + sol = convert(x, MyInt) + assert sol is x +- assert sys.getrefcount(x) == 3 # x + sol + 1 ++ assert sys.getrefcount(x) <= 3 # x + sol + 1 + + + class TestFloat: +@@ -535,10 +535,10 @@ class TestBinary: + + del sol + +- assert sys.getrefcount(msg) == 2 # msg + 1 ++ assert sys.getrefcount(msg) <= 2 # msg + 1 + sol = convert(msg, MyBytes) + assert sol is msg +- assert sys.getrefcount(msg) == 3 # msg + sol + 1 ++ assert sys.getrefcount(msg) <= 3 # msg + sol + 1 + + + class TestDateTime: +@@ -828,7 +828,7 @@ class TestEnum: + + msg = MyInt(1) + assert convert(msg, Ex) is Ex.x +- assert sys.getrefcount(msg) == 2 # msg + 1 ++ assert sys.getrefcount(msg) <= 2 # msg + 1 + assert convert(MyInt(2), Ex) is Ex.y + + def test_enum_missing(self): +@@ -2223,7 +2223,7 @@ class TestStructPostInit: + res = convert(msg, type=typ, from_attributes=from_attributes) + assert type(res) is Ex + assert called +- assert sys.getrefcount(singleton) == 2 # 1 for ref, 1 for call ++ assert sys.getrefcount(singleton) <= 2 # 1 for ref, 1 for call + + @pytest.mark.parametrize("union", [False, True]) + @pytest.mark.parametrize("exc_class", [ValueError, TypeError, OSError]) +Index: msgspec-0.19.0/tests/test_json.py +=================================================================== +--- msgspec-0.19.0.orig/tests/test_json.py ++++ msgspec-0.19.0/tests/test_json.py +@@ -898,7 +898,7 @@ class TestDatetime: + tz2 = msgspec.json.decode(msg, type=datetime.datetime).tzinfo + assert tz is tz2 + del tz2 +- assert sys.getrefcount(tz) == 3 # 1 tz, 1 cache, 1 func call ++ assert sys.getrefcount(tz) <= 3 # 1 tz, 1 cache, 1 func call + for _ in range(10): + gc.collect() # cache is cleared every 10 full collections + +@@ -2293,7 +2293,7 @@ class TestStruct: + assert x == Person("harry", "potter", 13, False) + + # one for struct, one for output of getattr, and one for getrefcount +- assert sys.getrefcount(x.first) == 3 ++ assert sys.getrefcount(x.first) <= 3 + + with pytest.raises( + msgspec.ValidationError, match="Expected `object`, got `int`" +Index: msgspec-0.19.0/tests/test_msgpack.py +=================================================================== +--- msgspec-0.19.0.orig/tests/test_msgpack.py ++++ msgspec-0.19.0/tests/test_msgpack.py +@@ -684,13 +684,13 @@ class TestTypedDecoder: + assert isinstance(res, memoryview) + assert bytes(res) == b"abcde" + if input_type is memoryview: +- assert sys.getrefcount(ref) == 3 ++ assert sys.getrefcount(ref) <= 3 + del msg +- assert sys.getrefcount(ref) == 3 ++ assert sys.getrefcount(ref) <= 3 + del res +- assert sys.getrefcount(ref) == 2 ++ assert sys.getrefcount(ref) <= 2 + elif input_type is bytes: +- assert sys.getrefcount(msg) == 3 ++ assert sys.getrefcount(msg) <= 3 + + def test_datetime_aware_ext(self): + dec = msgspec.msgpack.Decoder(datetime.datetime) +@@ -815,7 +815,7 @@ class TestTypedDecoder: + res = dec.decode(enc.encode(x)) + assert res == x + if res: +- assert sys.getrefcount(res[0]) == 3 # 1 tuple, 1 index, 1 func call ++ assert sys.getrefcount(res[0]) <= 3 # 1 tuple, 1 index, 1 func call + + @pytest.mark.parametrize("typ", [tuple, Tuple, Tuple[Any, ...]]) + def test_vartuple_any(self, typ): +Index: msgspec-0.19.0/tests/test_struct.py +=================================================================== +--- msgspec-0.19.0.orig/tests/test_struct.py ++++ msgspec-0.19.0/tests/test_struct.py +@@ -931,16 +931,16 @@ def test_struct_reference_counting(): + data = [1, 2, 3] + + t = Test(data) +- assert sys.getrefcount(data) == 3 ++ assert sys.getrefcount(data) <= 3 + + repr(t) +- assert sys.getrefcount(data) == 3 ++ assert sys.getrefcount(data) <= 3 + + t2 = t.__copy__() +- assert sys.getrefcount(data) == 4 ++ assert sys.getrefcount(data) <= 4 + + assert t == t2 +- assert sys.getrefcount(data) == 4 ++ assert sys.getrefcount(data) <= 4 + + + def test_struct_gc_not_added_if_not_needed(): +@@ -2581,7 +2581,7 @@ class TestPostInit: + Ex(1) + assert called + # Return value is decref'd +- assert sys.getrefcount(singleton) == 2 # 1 for ref, 1 for call ++ assert sys.getrefcount(singleton) <= 2 # 1 for ref, 1 for call + + def test_post_init_errors(self): + class Ex(Struct):