From b1663fb1924fb1680c8fbec97c2f727af65bd68e Mon Sep 17 00:00:00 2001 From: Robsdedude Date: Wed, 5 Nov 2025 13:22:14 +0100 Subject: [PATCH] Remove unused vector to/from native conversion functions --- changelog.d/70.clean.md | 2 + driver | 2 +- src/vector.rs | 68 +++- src/vector/native_conversion.rs | 332 ---------------- src/vector/swap_endian.rs | 66 ---- tests/benchmarks/test_vector_benchmarks.py | 33 -- tests/vector/from_driver/test_vector.py | 428 ++++++++++++--------- tests/vector/test_injection.py | 82 ---- 8 files changed, 301 insertions(+), 712 deletions(-) create mode 100644 changelog.d/70.clean.md delete mode 100644 src/vector/native_conversion.rs delete mode 100644 src/vector/swap_endian.rs diff --git a/changelog.d/70.clean.md b/changelog.d/70.clean.md new file mode 100644 index 0000000..5f24940 --- /dev/null +++ b/changelog.d/70.clean.md @@ -0,0 +1,2 @@ +Remove now unused helper functions for converting `Vector` values to/from native Python `lists`. +For more details, see [neo4j-python-driver#1263](https://github.com/neo4j/neo4j-python-driver/pull/1263). diff --git a/driver b/driver index 6ea4c12..d10d2cd 160000 --- a/driver +++ b/driver @@ -1 +1 @@ -Subproject commit 6ea4c12c66916d489b355d28a2cc0fdc2ab22549 +Subproject commit d10d2cd689340d9f2acfe67ca19131936e722f04 diff --git a/src/vector.rs b/src/vector.rs index ee453a5..d86a7b0 100644 --- a/src/vector.rs +++ b/src/vector.rs @@ -13,29 +13,65 @@ // See the License for the specific language governing permissions and // limitations under the License. -mod native_conversion; -mod swap_endian; +use pyo3::exceptions::PyValueError; +use pyo3::prelude::*; +use pyo3::types::{PyBytes, PyInt}; +use pyo3::{pyfunction, Bound, PyErr, PyResult}; use crate::register_package; -use pyo3::prelude::*; + +#[pyfunction] +fn swap_endian<'py>( + type_size: Bound<'py, PyInt>, + data: Bound<'py, PyBytes>, +) -> PyResult> { + let py = type_size.py(); + + let type_size: usize = match type_size.extract::() { + Ok(type_size @ 2) | Ok(type_size @ 4) | Ok(type_size @ 8) => type_size, + _ => { + return Err(PyErr::new::(format!( + "Unsupported type size {type_size}", + ))) + } + }; + let bytes = &data.as_bytes(); + let len = bytes.len(); + if len % type_size != 0 { + return Err(PyErr::new::( + "Data length not a multiple of type_size", + )); + } + + PyBytes::new_with(py, bytes.len(), |out| { + match type_size { + 2 => swap_n::<2>(bytes, out), + 4 => swap_n::<4>(bytes, out), + 8 => swap_n::<8>(bytes, out), + _ => unreachable!(), + } + Ok(()) + }) +} + +#[inline(always)] +fn swap_n(src: &[u8], dst: &mut [u8]) { + // Doesn't technically need to be a function with a const generic, but this + // allows the compiler to optimize the code better. + assert_eq!(src.len(), dst.len()); + assert_eq!(src.len() % N, 0); + for i in (0..src.len()).step_by(N) { + for j in 0..N { + dst[i + j] = src[i + N - j - 1]; + } + } +} pub(super) fn init_module(m: &Bound, name: &str) -> PyResult<()> { m.gil_used(false)?; register_package(m, name)?; - m.add_function(wrap_pyfunction!(swap_endian::swap_endian, m)?)?; - m.add_function(wrap_pyfunction!(native_conversion::vec_f64_from_native, m)?)?; - m.add_function(wrap_pyfunction!(native_conversion::vec_f64_to_native, m)?)?; - m.add_function(wrap_pyfunction!(native_conversion::vec_f32_from_native, m)?)?; - m.add_function(wrap_pyfunction!(native_conversion::vec_f32_to_native, m)?)?; - m.add_function(wrap_pyfunction!(native_conversion::vec_i64_from_native, m)?)?; - m.add_function(wrap_pyfunction!(native_conversion::vec_i64_to_native, m)?)?; - m.add_function(wrap_pyfunction!(native_conversion::vec_i32_from_native, m)?)?; - m.add_function(wrap_pyfunction!(native_conversion::vec_i32_to_native, m)?)?; - m.add_function(wrap_pyfunction!(native_conversion::vec_i16_from_native, m)?)?; - m.add_function(wrap_pyfunction!(native_conversion::vec_i16_to_native, m)?)?; - m.add_function(wrap_pyfunction!(native_conversion::vec_i8_from_native, m)?)?; - m.add_function(wrap_pyfunction!(native_conversion::vec_i8_to_native, m)?)?; + m.add_function(wrap_pyfunction!(swap_endian, m)?)?; Ok(()) } diff --git a/src/vector/native_conversion.rs b/src/vector/native_conversion.rs deleted file mode 100644 index ad98f2a..0000000 --- a/src/vector/native_conversion.rs +++ /dev/null @@ -1,332 +0,0 @@ -// Copyright (c) "Neo4j" -// Neo4j Sweden AB [https://neo4j.com] -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use pyo3::exceptions::{PyOverflowError, PyTypeError}; -use pyo3::prelude::*; -use pyo3::types::{PyBytes, PyFloat, PyInt, PyList}; - -// ================= -// ====== F64 ====== -// ================= - -#[pyfunction] -pub(super) fn vec_f64_from_native<'py>(data: Bound<'py, PyAny>) -> PyResult> { - let py = data.py(); - - let data_iter = data.try_iter()?; - let mut bytes = Vec::with_capacity(data_iter.size_hint().0.saturating_mul(size_of::())); - for value in data_iter { - let value = vec_value_as_f64(value?)?; - bytes.extend(&f64::to_be_bytes(value)); - } - Ok(PyBytes::new(py, &bytes)) -} - -fn vec_value_as_f64(value: Bound) -> PyResult { - fn make_error(value: &Bound) -> PyResult { - Err(PyErr::new::(format!( - "Cannot convert value to f64, expected float, got {}.", - value.get_type().name()? - ))) - } - - value - .cast::() - .or_else(|_| make_error(&value))? - .extract() - .or_else(|_| make_error(&value)) -} - -#[pyfunction] -pub(super) fn vec_f64_to_native<'py>(data: Bound<'py, PyBytes>) -> PyResult> { - const DATA_SIZE: usize = size_of::(); - let py = data.py(); - PyList::new( - py, - data.as_bytes().chunks(DATA_SIZE).map(|chunk| { - let value = f64::from_be_bytes( - chunk - .try_into() - .expect("bytes size is not multiple of type size"), - ); - PyFloat::new(py, value) - }), - ) -} - -// ================= -// ====== F32 ====== -// ================= - -#[pyfunction] -pub(super) fn vec_f32_from_native<'py>(data: Bound<'py, PyAny>) -> PyResult> { - let py = data.py(); - - let data_iter = data.try_iter()?; - let mut bytes = Vec::with_capacity(data_iter.size_hint().0.saturating_mul(size_of::())); - for value in data_iter { - let value = vec_value_as_f32(value?)?; - bytes.extend(&f32::to_be_bytes(value)); - } - Ok(PyBytes::new(py, &bytes)) -} - -fn vec_value_as_f32(value: Bound) -> PyResult { - fn make_error(value: &Bound) -> PyResult { - Err(PyErr::new::(format!( - "Cannot convert value to f32, expected float, got {}.", - value.get_type().name()? - ))) - } - - value - .cast::() - .or_else(|_| make_error(&value))? - .extract() - .or_else(|_| make_error(&value)) -} - -#[pyfunction] -pub(super) fn vec_f32_to_native<'py>(data: Bound<'py, PyBytes>) -> PyResult> { - const DATA_SIZE: usize = size_of::(); - let py = data.py(); - PyList::new( - py, - data.as_bytes().chunks(DATA_SIZE).map(|chunk| { - let value = f32::from_be_bytes( - chunk - .try_into() - .expect("bytes size is not multiple of type size"), - ); - PyFloat::new(py, value.into()) - }), - ) -} - -// ================= -// ====== I64 ====== -// ================= - -#[pyfunction] -pub(super) fn vec_i64_from_native<'py>(data: Bound<'py, PyAny>) -> PyResult> { - let py = data.py(); - - let data_iter = data.try_iter()?; - let mut bytes = Vec::with_capacity(data_iter.size_hint().0.saturating_mul(size_of::())); - for value in data_iter { - let value = vec_value_as_i64(value?)?; - bytes.extend(&i64::to_be_bytes(value)); - } - Ok(PyBytes::new(py, &bytes)) -} - -fn vec_value_as_i64(value: Bound) -> PyResult { - fn make_error(value: &Bound) -> PyResult { - Err(PyErr::new::(format!( - "Cannot convert value to i64, expected int, got {}.", - value.get_type().name()? - ))) - } - - let py = value.py(); - - let value = value.cast::().or_else(|_| make_error(&value))?; - if value.lt(PyInt::new(py, i64::MIN))? || value.gt(PyInt::new(py, i64::MAX))? { - return Err(PyErr::new::(format!( - "Value {} is out of range for i64: [-9223372036854775808, 9223372036854775807]", - value.str()? - ))); - } - value.extract().or_else(|_| make_error(value)) -} - -#[pyfunction] -pub(super) fn vec_i64_to_native<'py>(data: Bound<'py, PyBytes>) -> PyResult> { - const DATA_SIZE: usize = size_of::(); - let py = data.py(); - PyList::new( - py, - data.as_bytes().chunks(DATA_SIZE).map(|chunk| { - let value = i64::from_be_bytes( - chunk - .try_into() - .expect("bytes size is not multiple of type size"), - ); - PyInt::new(py, value) - }), - ) -} - -// ================= -// ====== I32 ====== -// ================= - -#[pyfunction] -pub(super) fn vec_i32_from_native<'py>(data: Bound<'py, PyAny>) -> PyResult> { - let py = data.py(); - - let data_iter = data.try_iter()?; - let mut bytes = Vec::with_capacity(data_iter.size_hint().0.saturating_mul(size_of::())); - for value in data_iter { - let value = vec_value_as_i32(value?)?; - bytes.extend(&i32::to_be_bytes(value)); - } - Ok(PyBytes::new(py, &bytes)) -} - -fn vec_value_as_i32(value: Bound) -> PyResult { - fn make_error(value: &Bound) -> PyResult { - Err(PyErr::new::(format!( - "Cannot convert value to i32, expected int, got {}.", - value.get_type().name()? - ))) - } - - let py = value.py(); - - let value = value.cast::().or_else(|_| make_error(&value))?; - if value.lt(PyInt::new(py, i32::MIN))? || value.gt(PyInt::new(py, i32::MAX))? { - return Err(PyErr::new::(format!( - "Value {} is out of range for i32: [-2147483648, 2147483647]", - value.str()? - ))); - } - value.extract().or_else(|_| make_error(value)) -} - -#[pyfunction] -pub(super) fn vec_i32_to_native<'py>(data: Bound<'py, PyBytes>) -> PyResult> { - const DATA_SIZE: usize = size_of::(); - let py = data.py(); - PyList::new( - py, - data.as_bytes().chunks(DATA_SIZE).map(|chunk| { - let value = i32::from_be_bytes( - chunk - .try_into() - .expect("bytes size is not multiple of type size"), - ); - PyInt::new(py, value) - }), - ) -} - -// ================= -// ====== I16 ====== -// ================= - -#[pyfunction] -pub(super) fn vec_i16_from_native<'py>(data: Bound<'py, PyAny>) -> PyResult> { - let py = data.py(); - - let data_iter = data.try_iter()?; - let mut bytes = Vec::with_capacity(data_iter.size_hint().0.saturating_mul(size_of::())); - for value in data_iter { - let value = vec_value_as_i16(value?)?; - bytes.extend(&i16::to_be_bytes(value)); - } - Ok(PyBytes::new(py, &bytes)) -} - -fn vec_value_as_i16(value: Bound) -> PyResult { - fn make_error(value: &Bound) -> PyResult { - Err(PyErr::new::(format!( - "Cannot convert value to i16, expected int, got {}.", - value.get_type().name()? - ))) - } - - let py = value.py(); - - let value = value.cast::().or_else(|_| make_error(&value))?; - if value.lt(PyInt::new(py, i16::MIN))? || value.gt(PyInt::new(py, i16::MAX))? { - return Err(PyErr::new::(format!( - "Value {} is out of range for i16: [-32768, 32767]", - value.str()? - ))); - } - value.extract().or_else(|_| make_error(value)) -} - -#[pyfunction] -pub(super) fn vec_i16_to_native<'py>(data: Bound<'py, PyBytes>) -> PyResult> { - const DATA_SIZE: usize = size_of::(); - let py = data.py(); - PyList::new( - py, - data.as_bytes().chunks(DATA_SIZE).map(|chunk| { - let value = i16::from_be_bytes( - chunk - .try_into() - .expect("bytes size is not multiple of type size"), - ); - PyInt::new(py, value) - }), - ) -} - -// ================ -// ====== I8 ====== -// ================ - -#[pyfunction] -pub(super) fn vec_i8_from_native<'py>(data: Bound<'py, PyAny>) -> PyResult> { - let py = data.py(); - - let data_iter = data.try_iter()?; - let mut bytes = Vec::with_capacity(data_iter.size_hint().0.saturating_mul(size_of::())); - for value in data_iter { - let value = vec_value_as_i8(value?)?; - bytes.extend(&i8::to_be_bytes(value)); - } - Ok(PyBytes::new(py, &bytes)) -} - -fn vec_value_as_i8(value: Bound) -> PyResult { - fn make_error(value: &Bound) -> PyResult { - Err(PyErr::new::(format!( - "Cannot convert value to i8, expected int, got {}.", - value.get_type().name()? - ))) - } - - let py = value.py(); - - let value = value.cast::().or_else(|_| make_error(&value))?; - if value.lt(PyInt::new(py, i8::MIN))? || value.gt(PyInt::new(py, i8::MAX))? { - return Err(PyErr::new::(format!( - "Value {} is out of range for i8: [-128, 127]", - value.str()? - ))); - } - value.extract().or_else(|_| make_error(value)) -} - -#[pyfunction] -pub(super) fn vec_i8_to_native<'py>(data: Bound<'py, PyBytes>) -> PyResult> { - const DATA_SIZE: usize = size_of::(); - let py = data.py(); - PyList::new( - py, - data.as_bytes().chunks(DATA_SIZE).map(|chunk| { - let value = i8::from_be_bytes( - chunk - .try_into() - .expect("bytes size is not multiple of type size"), - ); - PyInt::new(py, value) - }), - ) -} diff --git a/src/vector/swap_endian.rs b/src/vector/swap_endian.rs deleted file mode 100644 index 511d652..0000000 --- a/src/vector/swap_endian.rs +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) "Neo4j" -// Neo4j Sweden AB [https://neo4j.com] -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use pyo3::exceptions::PyValueError; -use pyo3::prelude::*; -use pyo3::types::{PyBytes, PyInt}; -use pyo3::{pyfunction, Bound, PyErr, PyResult}; - -#[pyfunction] -pub(super) fn swap_endian<'py>( - type_size: Bound<'py, PyInt>, - data: Bound<'py, PyBytes>, -) -> PyResult> { - let py = type_size.py(); - - let type_size: usize = match type_size.extract::() { - Ok(type_size @ 2) | Ok(type_size @ 4) | Ok(type_size @ 8) => type_size, - _ => { - return Err(PyErr::new::(format!( - "Unsupported type size {type_size}", - ))) - } - }; - let bytes = &data.as_bytes(); - let len = bytes.len(); - if len % type_size != 0 { - return Err(PyErr::new::( - "Data length not a multiple of type_size", - )); - } - - PyBytes::new_with(py, bytes.len(), |out| { - match type_size { - 2 => swap_n::<2>(bytes, out), - 4 => swap_n::<4>(bytes, out), - 8 => swap_n::<8>(bytes, out), - _ => unreachable!(), - } - Ok(()) - }) -} - -#[inline] -fn swap_n(src: &[u8], dst: &mut [u8]) { - // Doesn't technically need to be a function with a const generic, but this - // allows the compiler to optimize the code better. - assert_eq!(src.len(), dst.len()); - assert_eq!(src.len() % N, 0); - for i in (0..src.len()).step_by(N) { - for j in 0..N { - dst[i + j] = src[i + N - j - 1]; - } - } -} diff --git a/tests/benchmarks/test_vector_benchmarks.py b/tests/benchmarks/test_vector_benchmarks.py index c2cd6cc..7b0a8d3 100644 --- a/tests/benchmarks/test_vector_benchmarks.py +++ b/tests/benchmarks/test_vector_benchmarks.py @@ -21,7 +21,6 @@ from ..vector.from_driver.test_vector import ( _mock_mask_extensions, _swap_endian, - Vector, ) @@ -34,35 +33,3 @@ def test_bench_swap_endian(benchmark, mocker, ext, type_size, length): rounds = max(min(1_000_000 // length, 100_000), 100) benchmark.pedantic(lambda: _swap_endian(type_size, data), rounds=rounds) - - -@pytest.mark.parametrize("ext", ("numpy", "rust", "python")) -@pytest.mark.parametrize("dtype", ("i8", "i16", "i32", "i64", "f32", "f64")) -@pytest.mark.parametrize("as_gen", (True, False)) -@pytest.mark.parametrize("length", (1, 1_000)) -def test_bench_from_native(benchmark, mocker, ext, dtype, as_gen, length): - raw_data = bytes(i % 256 for i in range(8 * length)) - data = Vector.from_bytes(raw_data, dtype).to_native() - rounds = max(min(1_000_000 // length, 100_000), 100) - _mock_mask_extensions(mocker, ext) - if as_gen: - - def work(): - Vector.from_native((x for x in data), dtype) - else: - - def work(): - Vector.from_native(data, dtype) - - benchmark.pedantic(work, rounds=rounds) - - -@pytest.mark.parametrize("ext", ("numpy", "rust", "python")) -@pytest.mark.parametrize("dtype", ("i8", "i16", "i32", "i64", "f32", "f64")) -@pytest.mark.parametrize("length", (1, 1_000)) -def test_bench_to_native(benchmark, mocker, ext, dtype, length): - data = Vector.from_bytes(bytes(i % 256 for i in range(8 * length)), dtype) - rounds = max(min(1_000_000 // length, 100_000), 100) - _mock_mask_extensions(mocker, ext) - - benchmark.pedantic(data.to_native, rounds=rounds) diff --git a/tests/vector/from_driver/test_vector.py b/tests/vector/from_driver/test_vector.py index 7844517..d7702b4 100644 --- a/tests/vector/from_driver/test_vector.py +++ b/tests/vector/from_driver/test_vector.py @@ -16,6 +16,7 @@ from __future__ import annotations +import abc import math import random import struct @@ -39,6 +40,7 @@ if t.TYPE_CHECKING: import numpy import pyarrow + from pytest_mock import MockFixture T_ENDIAN_LITERAL: t.TypeAlias = t.Literal["big", "little"] | VectorEndian T_DTYPE_LITERAL: t.TypeAlias = ( @@ -57,6 +59,7 @@ T_DTYPE_FLOAT_LITERAL: t.TypeAlias = t.Literal[ "f32", "f64", VectorDType.F32, VectorDType.F64 ] + T_EXT_LITERAL: t.TypeAlias = t.Literal["numpy", "rust", "python"] ENDIAN_LITERALS: tuple[T_ENDIAN_LITERAL, ...] = ( @@ -152,17 +155,56 @@ def _get_type_size(dtype: str) -> t.Literal[1, 2, 4, 8]: return lookup[dtype] -def _normalize_float_bytes(dtype: str, data: bytes) -> bytes: - if dtype not in {"f32", "f64"}: - raise ValueError(f"Invalid dtype {dtype}") - type_size = _get_type_size(dtype) - pack_format = _dtype_to_pack_format(dtype) - chunks = (data[i : i + type_size] for i in range(0, len(data), type_size)) - return bytes( - b - for chunk in chunks - for b in struct.pack(pack_format, struct.unpack(pack_format, chunk)[0]) - ) +class NormalizableBytes(abc.ABC): + @abc.abstractmethod + def normalized_bytes(self) -> bytes: ... + + @abc.abstractmethod + def raw_bytes(self) -> bytes: ... + + +class Bytes(NormalizableBytes): + _data: bytes + + def __init__(self, data: bytes) -> None: + self._data = data + + def normalized_bytes(self) -> bytes: + return self._data + + def raw_bytes(self) -> bytes: + return self._data + + +class Float32NanPayloadBytes(NormalizableBytes): + _data: bytes + + def __init__(self, data: bytes) -> None: + self._data = data + + def normalized_bytes(self) -> bytes: + type_size = _get_type_size("f32") + pack_format = _dtype_to_pack_format("f32") + + # Python <3.14 does not preserve NaN payloads on struct pack/unpack + # for float32: + # https://github.com/python/cpython/issues/130317 + if sys.version_info >= (3, 14): + return self._data + chunks = ( + self._data[i : i + type_size] + for i in range(0, len(self._data), type_size) + ) + return bytes( + b + for chunk in chunks + for b in struct.pack( + pack_format, struct.unpack(pack_format, chunk)[0] + ) + ) + + def raw_bytes(self) -> bytes: + return self._data def _dtype_to_pack_format(dtype: str) -> str: @@ -176,20 +218,15 @@ def _dtype_to_pack_format(dtype: str) -> str: }[dtype] -def _mock_mask_extensions(mocker, used_ext): +def _mock_mask_extensions( + used_ext: T_EXT_LITERAL, mocker: MockFixture +) -> None: from neo4j.vector import ( _swap_endian_unchecked_np, _swap_endian_unchecked_py, _swap_endian_unchecked_rust, - _VecF32, - _VecF64, - _VecI8, - _VecI16, - _VecI32, - _VecI64, ) - vec_types = (_VecF64, _VecF32, _VecI64, _VecI32, _VecI16, _VecI8) match used_ext: case "numpy": if _swap_endian_unchecked_np is None: @@ -198,15 +235,6 @@ def _mock_mask_extensions(mocker, used_ext): "neo4j.vector._swap_endian_unchecked", new=_swap_endian_unchecked_np, ) - for vec_type in vec_types: - mocker.patch( - f"neo4j.vector.{vec_type.__name__}.from_native", - new=vec_type._from_native_np, - ) - mocker.patch( - f"neo4j.vector.{vec_type.__name__}.to_native", - new=vec_type._to_native_np, - ) case "rust": if _swap_endian_unchecked_rust is None: pytest.skip("rust extensions are not installed") @@ -214,37 +242,19 @@ def _mock_mask_extensions(mocker, used_ext): "neo4j.vector._swap_endian_unchecked", new=_swap_endian_unchecked_rust, ) - for vec_type in vec_types: - mocker.patch( - f"neo4j.vector.{vec_type.__name__}.from_native", - new=vec_type._from_native_rust, - ) - mocker.patch( - f"neo4j.vector.{vec_type.__name__}.to_native", - new=vec_type._to_native_rust, - ) case "python": mocker.patch( "neo4j.vector._swap_endian_unchecked", new=_swap_endian_unchecked_py, ) - for vec_type in vec_types: - mocker.patch( - f"neo4j.vector.{vec_type.__name__}.from_native", - new=vec_type._from_native_py, - ) - mocker.patch( - f"neo4j.vector.{vec_type.__name__}.to_native", - new=vec_type._to_native_py, - ) case _: raise ValueError(f"Invalid ext value {used_ext}") @pytest.mark.parametrize("ext", ("numpy", "rust", "python")) -def test_swap_endian(mocker, ext): +def test_swap_endian(mocker: MockFixture, ext: T_EXT_LITERAL) -> None: data = bytes(range(1, 17)) - _mock_mask_extensions(mocker, ext) + _mock_mask_extensions(ext, mocker) res = _swap_endian(2, data) assert isinstance(res, bytes) assert res == bytes( @@ -264,9 +274,11 @@ def test_swap_endian(mocker, ext): @pytest.mark.parametrize("ext", ("numpy", "rust", "python")) @pytest.mark.parametrize("type_size", (-1, 0, 3, 5, 7, 9, 16, 32)) -def test_swap_endian_unhandled_size(mocker, ext, type_size): +def test_swap_endian_unhandled_size( + ext: T_EXT_LITERAL, type_size: int, mocker: MockFixture +) -> None: data = bytes(i % 256 for i in range(1, abs(type_size) * 4)) - _mock_mask_extensions(mocker, ext) + _mock_mask_extensions(ext, mocker) with pytest.raises(ValueError, match=str(type_size)): _swap_endian(type_size, data) @@ -282,12 +294,12 @@ def test_swap_endian_unhandled_size(mocker, ext, type_size): ), pytest.param( "i8", - b"\x01", + bytes.fromhex("01"), id="i8-single", ), pytest.param( "i8", - b"\x01\x02\x03\x04", + bytes.fromhex("01020304"), id="i8-some", ), pytest.param( @@ -302,12 +314,12 @@ def test_swap_endian_unhandled_size(mocker, ext, type_size): ), pytest.param( "i16", - b"\x00\x01", + bytes.fromhex("0001"), id="i16-single", ), pytest.param( "i16", - b"\x00\x01\x00\x02", + bytes.fromhex("00010002"), id="i16-some", ), pytest.param( @@ -322,12 +334,12 @@ def test_swap_endian_unhandled_size(mocker, ext, type_size): ), pytest.param( "i32", - b"\x00\x00\x00\x01", + bytes.fromhex("00000001"), id="i32-single", ), pytest.param( "i32", - b"\x00\x00\x00\x01\x00\x00\x00\x02", + bytes.fromhex("0000000100000002"), id="i32-some", ), pytest.param( @@ -342,15 +354,12 @@ def test_swap_endian_unhandled_size(mocker, ext, type_size): ), pytest.param( "i64", - b"\x00\x00\x00\x00\x00\x00\x00\x01", + bytes.fromhex("0000000000000001"), id="i64-single", ), pytest.param( "i64", - ( - b"\x00\x00\x00\x00\x00\x00\x00\x01" - b"\x00\x00\x00\x00\x00\x00\x00\x02" - ), + bytes.fromhex("0000000000000001 0000000000000002"), id="i64-some", ), pytest.param( @@ -426,17 +435,13 @@ def nan_equals(a: list[object], b: list[object]) -> bool: @pytest.mark.parametrize("dtype", DTYPE_INT_LITERALS) @pytest.mark.parametrize(("repeat", "size"), ((10_000, 1), (1, 10_000))) -@pytest.mark.parametrize("ext", ("numpy", "rust", "python")) @pytest.mark.parametrize("use_init", (False, True)) def test_from_native_int_random( dtype: T_DTYPE_INT_LITERAL, repeat: int, size: int, - ext: str, use_init: bool, - mocker: t.Any, ) -> None: - _mock_mask_extensions(mocker, ext) type_size = _get_type_size(dtype) for _ in range(repeat): data = _random_value_be_bytes(type_size, size) @@ -452,24 +457,20 @@ def test_from_native_int_random( else: v = Vector.from_native(values, dtype) expected_raw = data - if dtype.startswith("f"): - expected_raw = _normalize_float_bytes(dtype, data) + if dtype == "f32": + expected_raw = Float32NanPayloadBytes(data).normalized_bytes() assert v.raw() == expected_raw @pytest.mark.parametrize("dtype", DTYPE_FLOAT_LITERALS) @pytest.mark.parametrize(("repeat", "size"), ((10_000, 1), (1, 10_000))) -@pytest.mark.parametrize("ext", ("numpy", "rust", "python")) @pytest.mark.parametrize("use_init", (False, True)) -def test_from_native_floatgst_random( +def test_from_native_float_random( dtype: T_DTYPE_FLOAT_LITERAL, repeat: int, size: int, - ext: str, use_init: bool, - mocker: t.Any, ) -> None: - _mock_mask_extensions(mocker, ext) type_size = _get_type_size(dtype) for _ in range(repeat): data = _random_value_be_bytes(type_size, size) @@ -485,156 +486,212 @@ def test_from_native_floatgst_random( else: v = Vector.from_native(values, dtype) expected_raw = data - if dtype.startswith("f"): - expected_raw = _normalize_float_bytes(dtype, data) + if dtype == "f32": + expected_raw = Float32NanPayloadBytes(data).normalized_bytes() assert v.raw() == expected_raw -SPECIAL_INT_VALUES: tuple[tuple[T_DTYPE_INT_LITERAL, int, bytes], ...] = ( +SPECIAL_INT_VALUES: tuple[ + tuple[T_DTYPE_INT_LITERAL, int, NormalizableBytes], ... +] = ( # (dtype, value, packed_bytes_be) # i8 - ("i8", -128, b"\x80"), - ("i8", 0, b"\x00"), - ("i8", 127, b"\x7f"), + ("i8", -128, Bytes(bytes.fromhex("80"))), + ("i8", 0, Bytes(bytes.fromhex("00"))), + ("i8", 127, Bytes(bytes.fromhex("7f"))), # i16 - ("i16", -32768, b"\x80\x00"), - ("i16", 0, b"\x00\x00"), - ("i16", 32767, b"\x7f\xff"), + ("i16", -32768, Bytes(bytes.fromhex("8000"))), + ("i16", 0, Bytes(bytes.fromhex("0000"))), + ("i16", 32767, Bytes(bytes.fromhex("7fff"))), # i32 - ("i32", -2147483648, b"\x80\x00\x00\x00"), - ("i32", 0, b"\x00\x00\x00\x00"), - ("i32", 2147483647, b"\x7f\xff\xff\xff"), + ("i32", -2147483648, Bytes(bytes.fromhex("80000000"))), + ("i32", 0, Bytes(bytes.fromhex("00000000"))), + ("i32", 2147483647, Bytes(bytes.fromhex("7fffffff"))), # i64 - ("i64", -9223372036854775808, b"\x80\x00\x00\x00\x00\x00\x00\x00"), - ("i64", 0, b"\x00\x00\x00\x00\x00\x00\x00\x00"), - ("i64", 9223372036854775807, b"\x7f\xff\xff\xff\xff\xff\xff\xff"), + ("i64", -9223372036854775808, Bytes(bytes.fromhex("8000000000000000"))), + ("i64", 0, Bytes(bytes.fromhex("0000000000000000"))), + ("i64", 9223372036854775807, Bytes(bytes.fromhex("7fffffffffffffff"))), ) SPECIAL_FLOAT_VALUES: tuple[ - tuple[T_DTYPE_FLOAT_LITERAL, float, bytes], ... + tuple[T_DTYPE_FLOAT_LITERAL, float, NormalizableBytes], ... ] = ( # (dtype, value, packed_bytes_be) # f32 # NaN - ("f32", float("nan"), b"\x7f\xc0\x00\x00"), - ("f32", float("-nan"), b"\xff\xc0\x00\x00"), ( "f32", - struct.unpack(">f", b"\x7f\xc0\x00\x11")[0], - b"\x7f\xc0\x00\x11", + float("nan"), + Bytes(bytes.fromhex("7fc00000")), + ), + ( + "f32", + float("-nan"), + Bytes(bytes.fromhex("ffc00000")), + ), + ( + "f32", + struct.unpack(">f", bytes.fromhex("7fc00011"))[0], + Bytes(bytes.fromhex("7fc00011")), ), ( "f32", - struct.unpack(">f", b"\x7f\x80\x00\x01")[0], - # Python < 3.14 does not properly preserver all NaN payload - # when calling struct.pack - _normalize_float_bytes("f32", b"\x7f\x80\x00\x01"), + struct.unpack(">f", bytes.fromhex("7f800001"))[0], + Float32NanPayloadBytes(bytes.fromhex("7f800001")), ), # ±inf - ("f32", float("inf"), b"\x7f\x80\x00\x00"), - ("f32", float("-inf"), b"\xff\x80\x00\x00"), + ( + "f32", + float("inf"), + Bytes(bytes.fromhex("7f800000")), + ), + ( + "f32", + float("-inf"), + Bytes(bytes.fromhex("ff800000")), + ), # ±0.0 - ("f32", 0.0, b"\x00\x00\x00\x00"), - ("f32", -0.0, b"\x80\x00\x00\x00"), + ( + "f32", + 0.0, + Bytes(bytes.fromhex("00000000")), + ), + ( + "f32", + -0.0, + Bytes(bytes.fromhex("80000000")), + ), # smallest normal ( "f32", - struct.unpack(">f", b"\x00\x80\x00\x00")[0], - b"\x00\x80\x00\x00", + struct.unpack(">f", bytes.fromhex("00800000"))[0], + Bytes(bytes.fromhex("00800000")), ), ( "f32", - struct.unpack(">f", b"\x80\x80\x00\x00")[0], - b"\x80\x80\x00\x00", + struct.unpack(">f", bytes.fromhex("80800000"))[0], + Bytes(bytes.fromhex("80800000")), ), # subnormal ( "f32", - struct.unpack(">f", b"\x00\x00\x00\x01")[0], - b"\x00\x00\x00\x01", + struct.unpack(">f", bytes.fromhex("00000001"))[0], + Bytes(bytes.fromhex("00000001")), ), ( "f32", - struct.unpack(">f", b"\x80\x00\x00\x01")[0], - b"\x80\x00\x00\x01", + struct.unpack(">f", bytes.fromhex("80000001"))[0], + Bytes(bytes.fromhex("80000001")), ), # largest normal ( "f32", - struct.unpack(">f", b"\x7f\x7f\xff\xff")[0], - b"\x7f\x7f\xff\xff", + struct.unpack(">f", bytes.fromhex("7f7fffff"))[0], + Bytes(bytes.fromhex("7f7fffff")), + ), + ( + "f32", + struct.unpack(">f", bytes.fromhex("ff7fffff"))[0], + Bytes(bytes.fromhex("ff7fffff")), ), + # very small f64 being rounded to ±0 in f32 ( "f32", - struct.unpack(">f", b"\xff\x7f\xff\xff")[0], - b"\xff\x7f\xff\xff", + struct.unpack(">d", bytes.fromhex("3686d601ad376ab9"))[0], + Bytes(bytes.fromhex("00000000")), + ), + ( + "f32", + struct.unpack(">d", bytes.fromhex("b686d601ad376ab9"))[0], + Bytes(bytes.fromhex("80000000")), ), # f64 # NaN - ("f64", float("nan"), b"\x7f\xf8\x00\x00\x00\x00\x00\x00"), - ("f64", float("-nan"), b"\xff\xf8\x00\x00\x00\x00\x00\x00"), ( "f64", - struct.unpack(">d", b"\x7f\xf8\x00\x00\x00\x00\x00\x11")[0], - b"\x7f\xf8\x00\x00\x00\x00\x00\x11", + float("nan"), + Bytes(bytes.fromhex("7ff8000000000000")), + ), + ( + "f64", + float("-nan"), + Bytes(bytes.fromhex("fff8000000000000")), + ), + ( + "f64", + struct.unpack(">d", bytes.fromhex("7ff8000000000011"))[0], + Bytes(bytes.fromhex("7ff8000000000011")), ), ( "f64", - struct.unpack(">d", b"\x7f\xf0\x00\x01\x00\x00\x00\x01")[0], - b"\x7f\xf0\x00\x01\x00\x00\x00\x01", + struct.unpack(">d", bytes.fromhex("7ff0000100000001"))[0], + Bytes(bytes.fromhex("7ff0000100000001")), ), # ±inf - ("f64", float("inf"), b"\x7f\xf0\x00\x00\x00\x00\x00\x00"), - ("f64", float("-inf"), b"\xff\xf0\x00\x00\x00\x00\x00\x00"), + ( + "f64", + float("inf"), + Bytes(bytes.fromhex("7ff0000000000000")), + ), + ( + "f64", + float("-inf"), + Bytes(bytes.fromhex("fff0000000000000")), + ), # ±0.0 - ("f64", 0.0, b"\x00\x00\x00\x00\x00\x00\x00\x00"), - ("f64", -0.0, b"\x80\x00\x00\x00\x00\x00\x00\x00"), + ( + "f64", + 0.0, + Bytes(bytes.fromhex("0000000000000000")), + ), + ( + "f64", + -0.0, + Bytes(bytes.fromhex("8000000000000000")), + ), # smallest normal ( "f64", - struct.unpack(">d", b"\x00\x10\x00\x00\x00\x00\x00\x00")[0], - b"\x00\x10\x00\x00\x00\x00\x00\x00", + struct.unpack(">d", bytes.fromhex("0010000000000000"))[0], + Bytes(bytes.fromhex("0010000000000000")), ), ( "f64", - struct.unpack(">d", b"\x80\x10\x00\x00\x00\x00\x00\x00")[0], - b"\x80\x10\x00\x00\x00\x00\x00\x00", + struct.unpack(">d", bytes.fromhex("8010000000000000"))[0], + Bytes(bytes.fromhex("8010000000000000")), ), # subnormal ( "f64", - struct.unpack(">d", b"\x00\x00\x00\x00\x00\x00\x00\x01")[0], - b"\x00\x00\x00\x00\x00\x00\x00\x01", + struct.unpack(">d", bytes.fromhex("0000000000000001"))[0], + Bytes(bytes.fromhex("0000000000000001")), ), ( "f64", - struct.unpack(">d", b"\x80\x00\x00\x00\x00\x00\x00\x01")[0], - b"\x80\x00\x00\x00\x00\x00\x00\x01", + struct.unpack(">d", bytes.fromhex("8000000000000001"))[0], + Bytes(bytes.fromhex("8000000000000001")), ), # largest normal ( "f64", - struct.unpack(">d", b"\x7f\xef\xff\xff\xff\xff\xff\xff")[0], - b"\x7f\xef\xff\xff\xff\xff\xff\xff", + struct.unpack(">d", bytes.fromhex("7fefffffffffffff"))[0], + Bytes(bytes.fromhex("7fefffffffffffff")), ), ( "f64", - struct.unpack(">d", b"\xff\xef\xff\xff\xff\xff\xff\xff")[0], - b"\xff\xef\xff\xff\xff\xff\xff\xff", + struct.unpack(">d", bytes.fromhex("ffefffffffffffff"))[0], + Bytes(bytes.fromhex("ffefffffffffffff")), ), ) SPECIAL_VALUES = SPECIAL_INT_VALUES + SPECIAL_FLOAT_VALUES -@pytest.mark.parametrize(("dtype", "value", "data_be"), SPECIAL_VALUES) -@pytest.mark.parametrize("ext", ("numpy", "rust", "python")) +@pytest.mark.parametrize(("dtype", "value", "data_be_raw"), SPECIAL_VALUES) def test_from_native_special_values( dtype: t.Literal["i8", "i16", "i32", "i64", "f32", "f64"], value: object, - data_be: bytes, - ext: str, - mocker: t.Any, + data_be_raw: NormalizableBytes, ) -> None: - _mock_mask_extensions(mocker, ext) + data_be = data_be_raw.normalized_bytes() if dtype in {"f32", "f64"}: assert isinstance(value, float) dtype_f = t.cast(t.Literal["f32", "f64"], dtype) @@ -671,14 +728,10 @@ def test_from_native_special_values( ("f64", 1), ), ) -@pytest.mark.parametrize("ext", ("numpy", "rust", "python")) def test_from_native_wrong_type( dtype: t.Literal["i8", "i16", "i32", "i64", "f32", "f64"], value: object, - ext: str, - mocker: t.Any, ) -> None: - _mock_mask_extensions(mocker, ext) with pytest.raises(TypeError) as exc: Vector.from_native([value], dtype) # type: ignore @@ -697,16 +750,22 @@ def test_from_native_wrong_type( ("i32", 2147483648), ("i64", -9223372036854775809), ("i64", 9223372036854775808), + # positive value, positive exponent overflow + ("f32", struct.unpack(">d", bytes.fromhex("47f0000020000000"))[0]), + # negative value, positive exponent overflow + ("f32", struct.unpack(">d", bytes.fromhex("c7f0000020000000"))[0]), + # no such thing as negative exponent overflow: + # very small values become 0.0 + # positive value, positive exponent, mantiassa overflow + ("f32", struct.unpack(">d", bytes.fromhex("47effffff0000000"))[0]), + # negative value, positive exponent, mantiassa overflow + ("f32", struct.unpack(">d", bytes.fromhex("c7effffff0000000"))[0]), ), ) -@pytest.mark.parametrize("ext", ("numpy", "rust", "python")) def test_from_native_overflow( dtype: t.Literal["i8", "i16", "i32", "i64", "f32", "f64"], value: object, - ext: str, - mocker: t.Any, ) -> None: - _mock_mask_extensions(mocker, ext) with pytest.raises(OverflowError) as exc: Vector.from_native([value], dtype) # type: ignore @@ -759,12 +818,13 @@ def test_to_native_random( assert nan_equals(v.to_native(), expected) -@pytest.mark.parametrize(("dtype", "value", "data_be"), SPECIAL_VALUES) +@pytest.mark.parametrize(("dtype", "value", "data_be_raw"), SPECIAL_VALUES) def test_to_native_special_values( dtype: t.Literal["i8", "i16", "i32", "i64", "f32", "f64"], value: object, - data_be: bytes, + data_be_raw: NormalizableBytes, ) -> None: + data_be = data_be_raw.raw_bytes() type_size = _get_type_size(dtype) pack_format = _dtype_to_pack_format(dtype) expected = [ @@ -829,14 +889,15 @@ def test_from_numpy_random( @pytest.mark.skipif(np is None, reason="numpy not installed") -@pytest.mark.parametrize(("dtype", "value", "data_be"), SPECIAL_VALUES) +@pytest.mark.parametrize(("dtype", "value", "data_be_raw"), SPECIAL_VALUES) @pytest.mark.parametrize("endian", ("big", "little", "native")) def test_from_numpy_special_values( dtype: t.Literal["i8", "i16", "i32", "i64", "f32", "f64"], endian: t.Literal["big", "little", "native"], value: object, - data_be: bytes, + data_be_raw: NormalizableBytes, ) -> None: + data_be = data_be_raw.raw_bytes() array = _get_numpy_array(data_be, dtype, endian) v = Vector.from_numpy(array) assert v.dtype == dtype @@ -873,7 +934,7 @@ def test_to_numpy_random( @pytest.mark.skipif(np is None, reason="numpy not installed") -@pytest.mark.parametrize(("dtype", "value", "data_be"), SPECIAL_VALUES) +@pytest.mark.parametrize(("dtype", "value", "data_be_raw"), SPECIAL_VALUES) @pytest.mark.parametrize( "endian", ( @@ -885,8 +946,9 @@ def test_to_numpy_special_values( dtype: t.Literal["i8", "i16", "i32", "i64", "f32", "f64"], endian: T_ENDIAN_LITERAL | None, value: object, - data_be: bytes, + data_be_raw: NormalizableBytes, ) -> None: + data_be = data_be_raw.raw_bytes() np_type = _get_numpy_dtype(dtype) v = _vector_from_data(data_be, dtype, endian) array = v.to_numpy() @@ -942,12 +1004,13 @@ def test_from_pyarrow_random( @pytest.mark.skipif(pa is None, reason="pyarrow not installed") -@pytest.mark.parametrize(("dtype", "value", "data_be"), SPECIAL_VALUES) +@pytest.mark.parametrize(("dtype", "value", "data_be_raw"), SPECIAL_VALUES) def test_from_pyarrow_special_values( dtype: t.Literal["i8", "i16", "i32", "i64", "f32", "f64"], value: object, - data_be: bytes, + data_be_raw: NormalizableBytes, ) -> None: + data_be = data_be_raw.raw_bytes() array = _get_pyarrow_array(data_be, dtype) v = Vector.from_pyarrow(array) assert v.dtype == dtype @@ -990,7 +1053,7 @@ def test_to_pyarrow_random( @pytest.mark.skipif(pa is None, reason="pyarrow not installed") -@pytest.mark.parametrize(("dtype", "value", "data_be"), SPECIAL_VALUES) +@pytest.mark.parametrize(("dtype", "value", "data_be_raw"), SPECIAL_VALUES) @pytest.mark.parametrize( "endian", ( @@ -1002,8 +1065,9 @@ def test_to_pyarrow_special_values( dtype: t.Literal["i8", "i16", "i32", "i64", "f32", "f64"], endian: T_ENDIAN_LITERAL | None, value: object, - data_be: bytes, + data_be_raw: NormalizableBytes, ) -> None: + data_be = data_be_raw.raw_bytes() type_size = _get_type_size(dtype) data_ne = data_be if sys.byteorder == "little": @@ -1021,31 +1085,26 @@ def test_to_pyarrow_special_values( @pytest.mark.parametrize( - ("vector", "expected"), + "vector", ( - (Vector([], "i8"), "Vector(b'', 'i8')"), - (Vector([], "i16"), "Vector(b'', 'i16')"), - (Vector([], "i32"), "Vector(b'', 'i32')"), - (Vector([], "i64"), "Vector(b'', 'i64')"), - (Vector([], "f32"), "Vector(b'', 'f32')"), - (Vector([], "f64"), "Vector(b'', 'f64')"), + Vector([], "i8"), + Vector([], "i16"), + Vector([], "i32"), + Vector([], "i64"), + Vector([], "f32"), + Vector([], "f64"), *( - ( - Vector([value], dtype), - f"Vector({packed_bytes_be!r}, {dtype!r})", - ) - for (dtype, value, packed_bytes_be) in SPECIAL_INT_VALUES + Vector([value], dtype) + for (dtype, value, packed_bytes_be_) in SPECIAL_INT_VALUES ), *( - ( - Vector([value], dtype), - f"Vector({packed_bytes_be!r}, {dtype!r})", - ) - for (dtype, value, packed_bytes_be) in SPECIAL_FLOAT_VALUES + Vector([value], dtype) + for (dtype, value, packed_bytes_be_) in SPECIAL_FLOAT_VALUES ), ), ) -def test_vector_repr(vector: Vector, expected: str) -> None: +def test_vector_repr(vector: Vector) -> None: + expected = f"Vector({vector.raw()!r}, {vector.dtype.value!r})" assert repr(vector) == expected @@ -1079,12 +1138,16 @@ def _dtype_to_cypher_type(dtype: T_DTYPE_LITERAL) -> str: }[dtype] -def _vec_element_cypher_repr(value: t.Any) -> str: - if isinstance(value, float): +def _vec_element_cypher_repr(value: t.Any, dtype: T_DTYPE_LITERAL) -> str: + if isinstance(value, float) and dtype in {"f32", "f64"}: if math.isnan(value): return "NaN" if math.isinf(value): return "Infinity" if value > 0 else "-Infinity" + if dtype == "f32": + # account for float32 precision loss + compressed = struct.unpack(">f", struct.pack(">f", value))[0] + return repr(compressed) return repr(value) @@ -1101,7 +1164,7 @@ def _vec_element_cypher_repr(value: t.Any) -> str: ( Vector([value], dtype), ( - f"vector([{_vec_element_cypher_repr(value)}], 1, " + f"vector([{_vec_element_cypher_repr(value, dtype)}], 1, " f"{_dtype_to_cypher_type(dtype)})" ), ) @@ -1111,7 +1174,7 @@ def _vec_element_cypher_repr(value: t.Any) -> str: ( Vector([value], dtype), ( - f"vector([{_vec_element_cypher_repr(value)}], 1, " + f"vector([{_vec_element_cypher_repr(value, dtype)}], 1, " f"{_dtype_to_cypher_type(dtype)})" ), ) @@ -1135,8 +1198,9 @@ def test_vector_str_random( for _ in range(repeat): data = _random_value_be_bytes(type_size, size) v = Vector(data, dtype) - values_repr = ( - f"[{', '.join(map(_vec_element_cypher_repr, v.to_native()))}]" + values_reprs = ( + _vec_element_cypher_repr(value, dtype) for value in v.to_native() ) + values_repr = f"[{', '.join(values_reprs)}]" expected = f"vector({values_repr}, {size}, {cypher_dtype})" assert str(v) == expected diff --git a/tests/vector/test_injection.py b/tests/vector/test_injection.py index edcff32..51e2749 100644 --- a/tests/vector/test_injection.py +++ b/tests/vector/test_injection.py @@ -14,8 +14,6 @@ # limitations under the License. -import pytest - import neo4j.vector @@ -30,83 +28,3 @@ def test_endian_swap_was_injected(mocker): mock = mocker.patch("neo4j.vector._swap_endian_unchecked") neo4j.vector._swap_endian(2, b"\x01\x02\x03\x04") mock.assert_called_once_with(2, b"\x01\x02\x03\x04") - - -@pytest.mark.parametrize( - "vec_cls", - ( - neo4j.vector._VecF64, - neo4j.vector._VecF32, - neo4j.vector._VecI64, - neo4j.vector._VecI32, - neo4j.vector._VecI16, - neo4j.vector._VecI8, - ), -) -def test_vec_from_native_was_imported(vec_cls): - vec_rust = neo4j.vector._vec_rust - assert vec_rust is not None - assert vec_cls.from_native == vec_cls._from_native_rust - - -@pytest.mark.parametrize( - ("dtype", "value", "method"), - ( - ("f64", 1.0, "vec_f64_from_native"), - ("f32", 1.0, "vec_f32_from_native"), - ("i64", 1, "vec_i64_from_native"), - ("i32", 1, "vec_i32_from_native"), - ("i16", 1, "vec_i16_from_native"), - ("i8", 1, "vec_i8_from_native"), - ), -) -def test_vec_from_native_was_injected(dtype, value, method, mocker): - mock = mocker.patch("neo4j.vector._vec_rust") - rust_mock = getattr(mock, method) - rust_mock.return_value = b"" - - data = [value] - - neo4j.vector.Vector.from_native(data, dtype) - - getattr(mock, method).assert_called_once_with(data) - - -@pytest.mark.parametrize( - "vec_cls", - ( - neo4j.vector._VecF64, - neo4j.vector._VecF32, - neo4j.vector._VecI64, - neo4j.vector._VecI32, - neo4j.vector._VecI16, - neo4j.vector._VecI8, - ), -) -def test_vec_to_native_was_imported(vec_cls): - vec_rust = neo4j.vector._vec_rust - assert vec_rust is not None - assert vec_cls.to_native == vec_cls._to_native_rust - - -@pytest.mark.parametrize( - ("dtype", "method"), - ( - ("f64", "vec_f64_to_native"), - ("f32", "vec_f32_to_native"), - ("i64", "vec_i64_to_native"), - ("i32", "vec_i32_to_native"), - ("i16", "vec_i16_to_native"), - ("i8", "vec_i8_to_native"), - ), -) -def test_vec_to_native_was_injected(dtype, method, mocker): - mock = mocker.patch("neo4j.vector._vec_rust") - - data = bytes(range(8)) - vec = neo4j.vector.Vector.from_bytes(data, dtype) - getattr(mock, method).assert_not_called() - - vec.to_native() - - getattr(mock, method).assert_called_once_with(data)