Add frozendict support for 3.15+ - #1052
Conversation
provinzkraut
left a comment
There was a problem hiding this comment.
Great stuff @sobolevn! Couple of questions :)
|
Done! Very helpful feedback, thanks a lot! |
provinzkraut
left a comment
There was a problem hiding this comment.
Just some thoughts on the docs, the rest looks good :)
| To achieve the best performance, | ||
| use native ``json`` types: ``list`` and ``dict``. | ||
| use collection types that can be constructed natively: | ||
| ``list``, ``set``, ``frozenset``, fixed-sized ``tuple``, and ``dict``. |
There was a problem hiding this comment.
Just curious, do you know why PySet_Add can add to a frozenset, but PyDict_SetItem cannot be used on a frozendict?
Was there a reasoning behind it?
|
Let's keep this open for some time to collect more feedback 👍 |
|
I don't think Siyet has done this yet so would either of you be interested in tackling my comment here?
We need to do the following, in order, for both performance (#310) and startup time (#884):
Pretty please try to prioritize this over other tasks and be sure to manually look at the drift of the existing profiling jobs before the next release 🙏 |
|
I created a public discussion about new potential C-API to efficently convert |
Siyet
left a comment
There was a problem hiding this comment.
Took a careful pass on this (including building the branch against 3.15.0a7) and found two issues to fix before merge. Nice feature otherwise, the type-system integration is clean.
1. Reference leak in every frozendict construction path.
PyFrozenDict_New(dict) does not steal its argument: for a plain dict it copies (via PyTuple_Pack + frozendict_new) and leaves the caller's reference intact (CPython Objects/dictobject.c, which is exactly why you proposed PyFrozenDict_FromDictSteal upstream). The wrappers do return PyFrozenDict_New(dict); without releasing the intermediate dict, so it leaks on every call: mpack_decode_frozendict, json_decode_frozendict, to_builtins_frozendict, convert_dict_to_frozendict, and the frozendict branch in convert_other.
Reproduced on 3.15.0a7, counting live dict objects after 5000 ops:
decode -> dict : +0
decode -> frozendict : +5000
convert -> frozendict : +5000
to_builtins frozendict : +5000
Fix at each site:
PyObject *res = PyFrozenDict_New(dict);
Py_DECREF(dict);
return res;(this also handles the case where New returns NULL). Worth a refcount regression test, since the functional tests pass straight through the leak.
2. msgspec.yaml.encode raises on frozendict.
to_builtins yields a frozendict, and PyYAML's SafeDumper has no representer for it:
msgspec.yaml.encode(frozendict({"a": 1}))
# RepresenterError: ('cannot represent an object', frozendict({'a': 1}))Same for a struct or dict containing a frozendict. (stdlib json.dumps(to_builtins(...)) happens to work, so it is specific to consumers that do not special-case frozendict, but yaml.encode is public API.) The yaml path likely needs to emit a plain dict for frozendict. A yaml roundtrip test would catch it.
Both reproduced on a local 3.15.0a7 build of this branch.
1476cd6 to
90a0b48
Compare
90a0b48 to
7fefc02
Compare
7fefc02 to
8928e21
Compare
ofek
left a comment
There was a problem hiding this comment.
Thanks for working on this! I wish we had better benchmarks (or added back the old profiling test dependency) so that we could assess the impact on performance.
| max_length: int | None = None | ||
|
|
||
|
|
||
| class FrozenDictType(Type): |
There was a problem hiding this comment.
I don't think that this is a good idea. Users might want to provide an API based on these types. It would require them to use conditional imports as well. I don't think that this extra complexity is needed. Saying that it won't be issued is enough IMO.
There was a problem hiding this comment.
For typing purposes, shouldn't we act as the responsible party here and enforce that? It seems to me that it's more correct to conditionally define this (just as our extension has conditional support) and far better to have static analysis catch errors rather than tests of runtime behavior.
There was a problem hiding this comment.
What runtime errors could arise from this?
Since this is inspection code, pre 3.15, you just never get back this type.
However, for consumers of this inspection, a conditional define would be awkward, e.g.
if isinstance(o, (inspect.FrozenDictType, inspect.DictType)):
...would have to become
if isinstance(o, inspect.DictType) or (sys.version_info >= (3, 15) and isinstance(o, inspect.FrozenDictType)):
...|
Done! Thanks a lot for the review! |
|
(I need one more approve now after the changes) 😅 |
| if use_encoder: | ||
| res = proto.Encoder(order=order).encode(msg) | ||
| else: | ||
| res = proto.encode(msg, order=order) |
There was a problem hiding this comment.
Shouldn't msg be converted to frozendict here?
Closes #1029