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
127 changes: 127 additions & 0 deletions test/collection/test_object_to_dict.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import datetime
import uuid

from weaviate.collections.classes.internal import (
CrossReference,
MetadataReturn,
Object,
)


def test_object_to_dict_basic() -> None:
"""A plain object with no references should round-trip into a JSON-serializable dict."""
obj_uuid = uuid.uuid4()
obj = Object(
uuid=obj_uuid,
metadata=MetadataReturn(),
properties={"name": "Alice", "age": 30},
references=None,
vector={},
collection="Person",
)

result = obj.to_dict()

assert result == {
"uuid": str(obj_uuid),
"metadata": {
"creation_time": None,
"last_update_time": None,
"distance": None,
"certainty": None,
"score": None,
"explain_score": None,
"is_consistent": None,
"rerank_score": None,
},
"properties": {"name": "Alice", "age": 30},
"references": {},
"vector": {},
"collection": "Person",
}


def test_object_to_dict_converts_datetime_and_uuid_properties() -> None:
"""`datetime` and `UUID` property values are not JSON-serializable by default and must be
converted to strings."""
created = datetime.datetime(2024, 2, 16, 12, 0, 0, tzinfo=datetime.timezone.utc)
friend_id = uuid.uuid4()
obj = Object(
uuid=uuid.uuid4(),
metadata=MetadataReturn(creation_time=created),
properties={"createdAt": created, "friendId": friend_id},
references=None,
vector={"default": [0.1, 0.2, 0.3]},
collection="Person",
)

result = obj.to_dict()

assert result["properties"]["createdAt"] == created.isoformat()
assert result["properties"]["friendId"] == str(friend_id)
assert result["metadata"]["creation_time"] == created.isoformat()
assert result["vector"] == {"default": [0.1, 0.2, 0.3]}


def test_object_to_dict_expands_cross_references() -> None:
"""Cross-referenced objects are recursively expanded into nested dicts rather than being
left as opaque `_CrossReference` instances."""
referenced_uuid = uuid.uuid4()
referenced_obj = Object(
uuid=referenced_uuid,
metadata=MetadataReturn(),
properties={"title": "Referenced"},
references=None,
vector={},
collection="Article",
)
obj = Object(
uuid=uuid.uuid4(),
metadata=MetadataReturn(),
properties={"name": "Bob"},
references={"wrote": CrossReference([referenced_obj])},
vector={},
collection="Person",
)

result = obj.to_dict()

assert result["references"] == {
"wrote": [
{
"uuid": str(referenced_uuid),
"metadata": {
"creation_time": None,
"last_update_time": None,
"distance": None,
"certainty": None,
"score": None,
"explain_score": None,
"is_consistent": None,
"rerank_score": None,
},
"properties": {"title": "Referenced"},
"references": {},
"vector": {},
"collection": "Article",
}
]
}


def test_cross_reference_repr_is_human_readable() -> None:
"""`repr()` of a `_CrossReference` should show its objects instead of a bare memory address."""
referenced_obj = Object(
uuid=uuid.uuid4(),
metadata=MetadataReturn(),
properties={"title": "Referenced"},
references=None,
vector={},
collection="Article",
)
ref = CrossReference([referenced_obj])

result = repr(ref)

assert result == f"CrossReference(objects={[referenced_obj]!r})"
assert "object at 0x" not in result
41 changes: 41 additions & 0 deletions weaviate/collections/classes/internal.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,18 @@
_QueryReferenceMultiTarget,
)
from weaviate.collections.classes.types import (
GeoCoordinate,
IReferences,
M,
P,
PhoneNumber,
Properties,
R,
References,
TProperties,
TReferences,
WeaviateProperties,
_PhoneNumber,
_WeaviateInput,
)
from weaviate.exceptions import (
Expand Down Expand Up @@ -120,6 +123,21 @@ class GroupByMetadataReturn:
distance: Optional[float] = None


def _weaviate_field_to_json(value: Any) -> Any:
"""Recursively convert a `WeaviateField` property value into a JSON-serializable value."""
if isinstance(value, datetime.datetime):
return value.isoformat()
if isinstance(value, uuid_package.UUID):
return str(value)
if isinstance(value, (GeoCoordinate, PhoneNumber, _PhoneNumber)):
return value.model_dump(mode="json")
if isinstance(value, Mapping):
return {k: _weaviate_field_to_json(v) for k, v in value.items()}
if isinstance(value, (list, tuple)):
return [_weaviate_field_to_json(v) for v in value]
return value


@dataclass
class _Object(Generic[P, R, M]):
uuid: uuid_package.UUID
Expand All @@ -129,6 +147,26 @@ class _Object(Generic[P, R, M]):
vector: Dict[str, Union[List[float], List[List[float]]]]
collection: str

def to_dict(self) -> Dict[str, Any]:
"""Convert this object into a JSON-serializable dictionary.

Cross-references are expanded recursively into their own `to_dict()` representation, so
take care when calling this on objects that are part of a reference cycle.
"""
return {
"uuid": str(self.uuid),
"metadata": {k: _weaviate_field_to_json(v) for k, v in vars(self.metadata).items()},
"properties": {
key: _weaviate_field_to_json(val) for key, val in self.properties.items()
},
"references": {
link: [obj.to_dict() for obj in ref.objects]
for link, ref in (self.references or {}).items()
},
"vector": self.vector,
"collection": self.collection,
}


@dataclass
class Object(Generic[P, R], _Object[P, R, MetadataReturn]):
Expand Down Expand Up @@ -535,6 +573,9 @@ def objects(self) -> List[Object[Properties, IReferences]]:
"""Returns the objects of the cross reference."""
return self.__objects or []

def __repr__(self) -> str:
return f"CrossReference(objects={self.objects!r})"


CrossReference: TypeAlias = _CrossReference[Properties, IReferences]
"""Use this TypeAlias when you want to type hint a cross reference within a generic data model.
Expand Down