Skip to content

Commit d369dd4

Browse files
committed
fix(python): keep legacy _zvec pickle paths loadable after the move
pybind11 records a class's defining module inside the pickle stream, so objects pickled by zvec <= 0.5.x reference the old top-level extension paths (`_zvec.param` / `_zvec.schema` / `_zvec.typing`). After moving the extension to `zvec._zvec`, loading such a pickle failed (ModuleNotFoundError, or a pybind "type already registered" error from re-importing the bare `_zvec`). Register `sys.modules` aliases for the legacy names in `zvec/__init__.py` so old pickles still load after upgrading, and add a regression test covering the old-layout -> new-layout pickle path.
1 parent e3b4298 commit d369dd4

2 files changed

Lines changed: 63 additions & 0 deletions

File tree

python/tests/test_pickle_compat.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Copyright 2025-present the zvec project
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
"""Legacy-pickle compatibility after the compiled extension moved from the
15+
top-level ``_zvec`` module to ``zvec._zvec``.
16+
17+
pybind11 records a class's defining module inside the pickle stream, so objects
18+
pickled by zvec <= 0.5.x reference ``_zvec.param`` / ``_zvec.schema`` /
19+
``_zvec.typing``. ``zvec/__init__.py`` registers ``sys.modules`` aliases so
20+
those pickles still load after upgrading.
21+
"""
22+
23+
from __future__ import annotations
24+
25+
import pickle
26+
import sys
27+
28+
from zvec.model.param import HnswIndexParam
29+
30+
31+
class TestLegacyPickleCompat:
32+
def test_legacy_module_aliases_registered(self):
33+
ext = sys.modules["zvec._zvec"]
34+
assert sys.modules.get("_zvec") is ext
35+
for sub in ("param", "schema", "typing"):
36+
assert sys.modules.get(f"_zvec.{sub}") is getattr(ext, sub)
37+
38+
def test_old_layout_pickle_still_loads(self):
39+
param = HnswIndexParam(m=16, ef_construction=200)
40+
# protocol 2 encodes globals as ``c<module>\n<qualname>\n`` (no length
41+
# prefix / framing), so we can rewrite the module path to the pre-0.5
42+
# top-level form to forge what an old wheel would have produced.
43+
blob = pickle.dumps(param, protocol=2)
44+
assert b"czvec._zvec.param\n" in blob
45+
legacy = blob.replace(b"czvec._zvec.param\n", b"c_zvec.param\n")
46+
47+
restored = pickle.loads(legacy)
48+
assert restored.m == 16

python/zvec/__init__.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,21 @@
5858
load_diskann_plugin,
5959
)
6060

61+
# —— Backwards compatibility for legacy pickles ——
62+
# zvec <= 0.5.x shipped the compiled extension at the top level as ``_zvec``
63+
# (with submodules ``_zvec.param`` / ``_zvec.schema`` / ``_zvec.typing``); it
64+
# now lives at ``zvec._zvec``. pybind11 records a class's defining module inside
65+
# the pickle stream, so objects pickled by an old wheel reference the legacy
66+
# paths. Alias the old names in ``sys.modules`` so those pickles still load
67+
# after upgrading. (``zvec._zvec`` is already imported by the re-exports above.)
68+
_legacy_ext = sys.modules["zvec._zvec"]
69+
sys.modules.setdefault("_zvec", _legacy_ext)
70+
for _legacy_sub in ("param", "schema", "typing"):
71+
_legacy_mod = getattr(_legacy_ext, _legacy_sub, None)
72+
if _legacy_mod is not None:
73+
sys.modules.setdefault(f"_zvec.{_legacy_sub}", _legacy_mod)
74+
del _legacy_ext, _legacy_sub, _legacy_mod
75+
6176
from . import model as model
6277

6378
# —— Extensions ——

0 commit comments

Comments
 (0)