Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion msgspec/_core.c
Original file line number Diff line number Diff line change
Expand Up @@ -1886,14 +1886,14 @@ Meta_new(PyTypeObject *type, PyObject *args, PyObject *kwargs) {
Meta *out = (Meta *)Meta_Type.tp_alloc(&Meta_Type, 0);
if (out == NULL) return NULL;

// set fields on Meta and increase their refcount
#define SET_FIELD(x) do { Py_XINCREF(x); out->x = x; } while(0)
SET_FIELD(gt);
SET_FIELD(ge);
SET_FIELD(lt);
SET_FIELD(le);
SET_FIELD(multiple_of);
SET_FIELD(pattern);
SET_FIELD(regex);
SET_FIELD(min_length);
SET_FIELD(max_length);
SET_FIELD(tz);
Expand All @@ -1903,6 +1903,11 @@ Meta_new(PyTypeObject *type, PyObject *args, PyObject *kwargs) {
SET_FIELD(extra_json_schema);
SET_FIELD(extra);
#undef SET_FIELD

// set fields on Meta without increasing their refcount
// regex was created by a PyObject_CallOneArg call, so refcount started out as 1; no need to increase
out->regex = regex;

return (PyObject *)out;
}

Expand Down
27 changes: 27 additions & 0 deletions tests/test_struct_meta.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
"""Tests for the exposed StructMeta metaclass."""

import gc
import re
import secrets

import pytest
import msgspec
from msgspec import Struct, StructMeta
Expand Down Expand Up @@ -362,3 +366,26 @@ class Container(metaclass=EncoderMeta):
assert decoded.count == 1
assert decoded.item.id == 123
assert decoded.item.name == "test"


def test_struct_meta_pattern_ref_leak():
# ensure that we're not keeping around references to re.Pattern longer than necessary
# see https://github.com/jcrist/msgspec/pull/899 for details

# clear cache to get a baseline
re.purge()

# use a random string to create a pattern, to ensure there can never be an overlap
# with any cached pattern
pattern_string = secrets.token_hex()
msgspec.Meta(pattern=pattern_string)
# purge cache and gc again
re.purge()
gc.collect()
# there should be no re.Pattern any more with our pattern anymore. if there is, it's
# being kept alive by some reference
assert not any(
o
for o in gc.get_objects()
if isinstance(o, re.Pattern) and o.pattern == pattern_string
)