Skip to content

Commit 77bce40

Browse files
committed
fix(embedded): keep the value tags in a standalone install
The tags came from `coordinode`, which the embedded engine deliberately does not depend on, so installing it alone left the lookup returning None and both new types degrading to the plain container they were tagged to be told apart from. The fallback was silent: a multi-vector read back re-encoded as an array and a path as a map, exactly what the tags exist to prevent. They now live in `coordinode_embedded._types`, which re-exports `coordinode`'s definitions when that package is present and defines equivalents when it is not. With both installed the two names are the same class object, so a value tagged through either is recognised by the other. Both are exported from their packages as well. Only an instance activates the multi-vector and path wire types, so without a public constructor there was no supported way to send one. Verified in a venv holding the wheel alone, where `coordinode._types` is not importable: the round-trip tests pass there rather than falling back.
1 parent 00aed36 commit 77bce40

5 files changed

Lines changed: 57 additions & 13 deletions

File tree

coordinode-embedded/python/coordinode_embedded/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,6 @@
2222
"""
2323

2424
from ._coordinode_embedded import Hnsw, LocalClient
25+
from ._types import MultiVector, Path
2526

26-
__all__ = ["Hnsw", "LocalClient"]
27+
__all__ = ["Hnsw", "LocalClient", "MultiVector", "Path"]
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
"""Value types whose wire form a plain Python container cannot express.
2+
3+
A multi-vector is a list of lists and a path is a mapping, so neither can be
4+
told apart from the ordinary container it looks like once it has been handed
5+
to Python. These tags carry the distinction, and the conversion layer checks
6+
for them ahead of the generic list and dict branches: without one, reading a
7+
value and writing it straight back stores it as something else.
8+
9+
`coordinode` owns the canonical definitions, and this package deliberately
10+
does not depend on it: the embedded engine is usable on its own. So when that
11+
package is present its classes are re-used here, and a value tagged through
12+
one is recognised by the other; a standalone install falls back to the
13+
equivalents below.
14+
"""
15+
16+
from __future__ import annotations
17+
18+
try: # pragma: no cover - exercised by whichever install shape is in use
19+
from coordinode._types import MultiVector, Path
20+
except ImportError:
21+
22+
class MultiVector(list): # type: ignore[no-redef]
23+
"""Several equal-width vectors describing one item."""
24+
25+
__slots__ = ()
26+
27+
class Path(dict): # type: ignore[no-redef]
28+
"""A graph path: the node ids it runs through, and the hops between."""
29+
30+
__slots__ = ()
31+
32+
33+
__all__ = ["MultiVector", "Path"]

coordinode-embedded/src/lib.rs

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -26,26 +26,27 @@ use rmpv::Value as MsgpackValue;
2626

2727
// ── Value → PyObject conversion ──────────────────────────────────────────────
2828

29-
/// The `MultiVector` tag from the `coordinode` package, when it is installed.
29+
/// The `MultiVector` tag, from this package's own `_types` module.
3030
///
3131
/// A list subclass, so a tagged value reads and compares exactly like the
32-
/// nested list it replaces; the type exists only to survive a round trip. This
33-
/// package does not depend on `coordinode` (the embedded engine is usable on
34-
/// its own), so a missing import is an ordinary outcome and the caller falls
35-
/// back to a plain list, which is what this returned before the tag existed.
32+
/// nested list it replaces; the type exists only to survive a round trip,
33+
/// since an untagged nested list re-encodes as an array and changes the
34+
/// property's type. That module re-exports `coordinode`'s definition when that
35+
/// package is installed and defines an equivalent when it is not, so the tag
36+
/// holds for a standalone install of the embedded engine too.
3637
fn multi_vector_type(py: Python<'_>) -> Option<Bound<'_, PyAny>> {
37-
py.import("coordinode._types")
38+
py.import("coordinode_embedded._types")
3839
.and_then(|m| m.getattr("MultiVector"))
3940
.ok()
4041
}
4142

42-
/// The `Path` tag from the `coordinode` package, when it is installed.
43+
/// The `Path` tag, from this package's own `_types` module.
4344
///
44-
/// A dict subclass, for the same reason and with the same fallback as
45-
/// [`multi_vector_type`]: without the tag a path read back is an ordinary
46-
/// mapping, and writing it out again would store a map.
45+
/// A dict subclass, for the same reason as [`multi_vector_type`]: without the
46+
/// tag a path read back is an ordinary mapping, and writing it out again would
47+
/// store a map.
4748
fn path_type(py: Python<'_>) -> Option<Bound<'_, PyAny>> {
48-
py.import("coordinode._types")
49+
py.import("coordinode_embedded._types")
4950
.and_then(|m| m.getattr("Path"))
5051
.ok()
5152
}

coordinode/coordinode/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
result = await db.cypher("MATCH (n) RETURN count(n) AS total")
1919
"""
2020

21+
from coordinode._types import MultiVector, Path
2122
from coordinode.client import (
2223
AsyncCoordinodeClient,
2324
CoordinodeClient,
@@ -39,6 +40,11 @@
3940
__all__ = [
4041
"CoordinodeClient",
4142
"AsyncCoordinodeClient",
43+
# Values whose wire type only a tag can carry: a plain nested list encodes
44+
# as a list and a plain dict as a map, so sending either of these types
45+
# requires the constructor, not just the shape.
46+
"MultiVector",
47+
"Path",
4248
"NodeResult",
4349
"EdgeResult",
4450
"VectorResult",

tests/unit/test_embedded_values.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@
1010

1111
ce = pytest.importorskip("coordinode_embedded")
1212

13-
from coordinode._types import MultiVector, Path # noqa: E402 (after the skip guard)
13+
# From the embedded package rather than `coordinode`, so this file runs where
14+
# only the wheel is installed. It re-exports `coordinode`'s definitions when
15+
# that package is present, so this is the same class either way.
16+
from coordinode_embedded import MultiVector, Path # noqa: E402 (after the skip guard)
1417

1518

1619
@pytest.fixture

0 commit comments

Comments
 (0)