From 5275164edb3a2847d4a5aec1af5ae90f7dc47b53 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Thu, 8 Oct 2026 18:42:26 -0700 Subject: [PATCH 01/35] Version bump to 0.5.1 [skip ci] --- CHANGELOG.md | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cd577c8..666ca29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -## 0.5.1 (unreleased) +## 0.5.1 (2026-10-08) - Fixed error with non-canonical SciPy sparse arrays - Fixed error with SciPy sparse arrays with explicit zeros diff --git a/pyproject.toml b/pyproject.toml index ad130b5..3ff09d9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "pgvector" -version = "0.5.0" +version = "0.5.1" description = "pgvector support for Python" readme = "README.md" authors = [ From 62d9388d2572c21e6abc0de6023c62cf58b40251 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sat, 10 Oct 2026 14:23:22 -0700 Subject: [PATCH 02/35] Fixed tests for SQLAlchemy 2.1 [skip ci] --- tests/test_sqlalchemy.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/tests/test_sqlalchemy.py b/tests/test_sqlalchemy.py index 3dd7bac..eef30b3 100644 --- a/tests/test_sqlalchemy.py +++ b/tests/test_sqlalchemy.py @@ -12,6 +12,12 @@ from typing import Any from .conftest import numpy as np +try: + from sqlalchemy.dialects.postgresql import BitString + sqlalchemy_version = 2.1 +except ImportError: + sqlalchemy_version = 2 + psycopg2_engine = create_engine('postgresql+psycopg2://localhost/pgvector_python_test') psycopg2_type_engine = create_engine('postgresql+psycopg2://localhost/pgvector_python_test') @@ -420,7 +426,7 @@ def test_select(self, engine: Engine) -> None: def test_select_orm(self, engine: Engine) -> None: with Session(engine) as session: session.add(Item(embedding=[2, 3, 3])) - items = session.scalars(select(Item.embedding.l2_distance([1, 1, 1]))).all() + items = session.scalars(select(Item.embedding.l2_distance([1, 1, 1]))).all() # type: ignore assert items == [3] def test_avg(self, engine: Engine) -> None: @@ -634,9 +640,15 @@ async def test_bit(self, engine: AsyncEngine) -> None: async with async_session() as session: async with session.begin(): - # typing issue - # https://github.com/MagicStack/py-pgproto/pull/32 - embedding = asyncpg.BitString('101') if engine == asyncpg_engine else '101' # type: ignore + embedding: Any + if sqlalchemy_version >= 2.1: + embedding = BitString('101') + elif engine == asyncpg_engine: + # typing issue + # https://github.com/MagicStack/py-pgproto/pull/32 + embedding = asyncpg.BitString('101') # type: ignore + else: + embedding = '101' session.add(Item(id=1, binary_embedding=embedding)) item = await session.get_one(Item, 1) assert item.binary_embedding == embedding From 0c33b6dd187e25fdd6048781999b09a2f11fa28b Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sat, 10 Oct 2026 14:23:43 -0700 Subject: [PATCH 03/35] Updated CI --- .github/workflows/build.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 952be69..5a01635 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,10 +6,10 @@ jobs: strategy: fail-fast: false matrix: - python: [3.14, "3.10"] + python: [3.15, "3.10"] steps: - uses: actions/checkout@v7 - - uses: actions/setup-python@v6 + - uses: actions/setup-python@v7 with: python-version: ${{ matrix.python }} - run: pip install --group dev @@ -19,7 +19,7 @@ jobs: dev-files: true - run: | cd /tmp - git clone --branch v0.8.4 https://github.com/pgvector/pgvector.git + git clone --branch v0.8.7 https://github.com/pgvector/pgvector.git cd pgvector make sudo make install From ffdcea9ebe686fdc5a17e8bb2c16c0805975dfc0 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sat, 10 Oct 2026 14:37:13 -0700 Subject: [PATCH 04/35] Fixed CI --- tests/test_sqlalchemy.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/tests/test_sqlalchemy.py b/tests/test_sqlalchemy.py index eef30b3..dc58aeb 100644 --- a/tests/test_sqlalchemy.py +++ b/tests/test_sqlalchemy.py @@ -4,6 +4,7 @@ from pgvector.sqlalchemy import VECTOR, HALFVEC, BIT, SPARSEVEC, avg, sum import pytest from sqlalchemy import create_engine, event, insert, inspect, literal, select, text, MetaData, Table, Column, Index, Integer, ARRAY, Engine +from sqlalchemy.dialects import postgresql from sqlalchemy.exc import StatementError from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine, AsyncEngine from sqlalchemy.ext.automap import automap_base @@ -12,11 +13,7 @@ from typing import Any from .conftest import numpy as np -try: - from sqlalchemy.dialects.postgresql import BitString - sqlalchemy_version = 2.1 -except ImportError: - sqlalchemy_version = 2 +sqlalchemy_version = 2.1 if hasattr(postgresql, 'BitString') else 2 psycopg2_engine = create_engine('postgresql+psycopg2://localhost/pgvector_python_test') psycopg2_type_engine = create_engine('postgresql+psycopg2://localhost/pgvector_python_test') @@ -642,7 +639,7 @@ async def test_bit(self, engine: AsyncEngine) -> None: async with session.begin(): embedding: Any if sqlalchemy_version >= 2.1: - embedding = BitString('101') + embedding = postgresql.BitString('101') # type: ignore elif engine == asyncpg_engine: # typing issue # https://github.com/MagicStack/py-pgproto/pull/32 From 57b8e24ed659cba1bcd646b3abddea5a3855d63e Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sat, 10 Oct 2026 14:47:59 -0700 Subject: [PATCH 05/35] Fixed typing --- pgvector/sqlalchemy/bit.py | 4 +--- pgvector/sqlalchemy/halfvec.py | 4 +--- pgvector/sqlalchemy/sparsevec.py | 4 +--- pgvector/sqlalchemy/vector.py | 4 +--- 4 files changed, 4 insertions(+), 12 deletions(-) diff --git a/pgvector/sqlalchemy/bit.py b/pgvector/sqlalchemy/bit.py index 7c861ef..56b5dcc 100644 --- a/pgvector/sqlalchemy/bit.py +++ b/pgvector/sqlalchemy/bit.py @@ -14,11 +14,9 @@ def process_bind_param(self, value: Any, dialect: Any) -> Any: return asyncpg.BitString(value) # type: ignore return value - class Comparator(TypeDecorator.Comparator[Any]): + class comparator_factory(TypeDecorator.Comparator[Any]): def hamming_distance(self, other: object, /) -> Operators: return self.op('<~>', return_type=Float)(other) def jaccard_distance(self, other: object, /) -> Operators: return self.op('<%>', return_type=Float)(other) - - comparator_factory = Comparator # type: ignore diff --git a/pgvector/sqlalchemy/halfvec.py b/pgvector/sqlalchemy/halfvec.py index 9d717c3..e726e0f 100644 --- a/pgvector/sqlalchemy/halfvec.py +++ b/pgvector/sqlalchemy/halfvec.py @@ -35,7 +35,7 @@ def process(value: Any) -> list[float] | None: return HalfVector._from_db(value) return process - class Comparator(TypeEngine.Comparator[Any]): + class comparator_factory(TypeEngine.Comparator[Any]): def l2_distance(self, other: object, /) -> Operators: return self.op('<->', return_type=Float)(other) @@ -48,8 +48,6 @@ def cosine_distance(self, other: object, /) -> Operators: def l1_distance(self, other: object, /) -> Operators: return self.op('<+>', return_type=Float)(other) - comparator_factory = Comparator - # for reflection ischema_names['halfvec'] = HALFVEC # type: ignore diff --git a/pgvector/sqlalchemy/sparsevec.py b/pgvector/sqlalchemy/sparsevec.py index 9bd1478..2c94104 100644 --- a/pgvector/sqlalchemy/sparsevec.py +++ b/pgvector/sqlalchemy/sparsevec.py @@ -35,7 +35,7 @@ def process(value: Any) -> SparseVector | None: return SparseVector._from_db(value) return process - class Comparator(TypeEngine.Comparator[Any]): + class comparator_factory(TypeEngine.Comparator[Any]): def l2_distance(self, other: object, /) -> Operators: return self.op('<->', return_type=Float)(other) @@ -48,8 +48,6 @@ def cosine_distance(self, other: object, /) -> Operators: def l1_distance(self, other: object, /) -> Operators: return self.op('<+>', return_type=Float)(other) - comparator_factory = Comparator - # for reflection ischema_names['sparsevec'] = SPARSEVEC # type: ignore diff --git a/pgvector/sqlalchemy/vector.py b/pgvector/sqlalchemy/vector.py index 83acb09..b86c91d 100644 --- a/pgvector/sqlalchemy/vector.py +++ b/pgvector/sqlalchemy/vector.py @@ -35,7 +35,7 @@ def process(value: Any) -> list[float] | None: return Vector._from_db(value) return process - class Comparator(TypeEngine.Comparator[Any]): + class comparator_factory(TypeEngine.Comparator[Any]): def l2_distance(self, other: object, /) -> Operators: return self.op('<->', return_type=Float)(other) @@ -48,8 +48,6 @@ def cosine_distance(self, other: object, /) -> Operators: def l1_distance(self, other: object, /) -> Operators: return self.op('<+>', return_type=Float)(other) - comparator_factory = Comparator - # for reflection ischema_names['vector'] = VECTOR # type: ignore From 40ef30fa67373ac613c5764e240a2c8806555b94 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sat, 10 Oct 2026 15:05:10 -0700 Subject: [PATCH 06/35] Revert "Fixed typing" This reverts commit 57b8e24ed659cba1bcd646b3abddea5a3855d63e. --- pgvector/sqlalchemy/bit.py | 4 +++- pgvector/sqlalchemy/halfvec.py | 4 +++- pgvector/sqlalchemy/sparsevec.py | 4 +++- pgvector/sqlalchemy/vector.py | 4 +++- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/pgvector/sqlalchemy/bit.py b/pgvector/sqlalchemy/bit.py index 56b5dcc..7c861ef 100644 --- a/pgvector/sqlalchemy/bit.py +++ b/pgvector/sqlalchemy/bit.py @@ -14,9 +14,11 @@ def process_bind_param(self, value: Any, dialect: Any) -> Any: return asyncpg.BitString(value) # type: ignore return value - class comparator_factory(TypeDecorator.Comparator[Any]): + class Comparator(TypeDecorator.Comparator[Any]): def hamming_distance(self, other: object, /) -> Operators: return self.op('<~>', return_type=Float)(other) def jaccard_distance(self, other: object, /) -> Operators: return self.op('<%>', return_type=Float)(other) + + comparator_factory = Comparator # type: ignore diff --git a/pgvector/sqlalchemy/halfvec.py b/pgvector/sqlalchemy/halfvec.py index e726e0f..9d717c3 100644 --- a/pgvector/sqlalchemy/halfvec.py +++ b/pgvector/sqlalchemy/halfvec.py @@ -35,7 +35,7 @@ def process(value: Any) -> list[float] | None: return HalfVector._from_db(value) return process - class comparator_factory(TypeEngine.Comparator[Any]): + class Comparator(TypeEngine.Comparator[Any]): def l2_distance(self, other: object, /) -> Operators: return self.op('<->', return_type=Float)(other) @@ -48,6 +48,8 @@ def cosine_distance(self, other: object, /) -> Operators: def l1_distance(self, other: object, /) -> Operators: return self.op('<+>', return_type=Float)(other) + comparator_factory = Comparator + # for reflection ischema_names['halfvec'] = HALFVEC # type: ignore diff --git a/pgvector/sqlalchemy/sparsevec.py b/pgvector/sqlalchemy/sparsevec.py index 2c94104..9bd1478 100644 --- a/pgvector/sqlalchemy/sparsevec.py +++ b/pgvector/sqlalchemy/sparsevec.py @@ -35,7 +35,7 @@ def process(value: Any) -> SparseVector | None: return SparseVector._from_db(value) return process - class comparator_factory(TypeEngine.Comparator[Any]): + class Comparator(TypeEngine.Comparator[Any]): def l2_distance(self, other: object, /) -> Operators: return self.op('<->', return_type=Float)(other) @@ -48,6 +48,8 @@ def cosine_distance(self, other: object, /) -> Operators: def l1_distance(self, other: object, /) -> Operators: return self.op('<+>', return_type=Float)(other) + comparator_factory = Comparator + # for reflection ischema_names['sparsevec'] = SPARSEVEC # type: ignore diff --git a/pgvector/sqlalchemy/vector.py b/pgvector/sqlalchemy/vector.py index b86c91d..83acb09 100644 --- a/pgvector/sqlalchemy/vector.py +++ b/pgvector/sqlalchemy/vector.py @@ -35,7 +35,7 @@ def process(value: Any) -> list[float] | None: return Vector._from_db(value) return process - class comparator_factory(TypeEngine.Comparator[Any]): + class Comparator(TypeEngine.Comparator[Any]): def l2_distance(self, other: object, /) -> Operators: return self.op('<->', return_type=Float)(other) @@ -48,6 +48,8 @@ def cosine_distance(self, other: object, /) -> Operators: def l1_distance(self, other: object, /) -> Operators: return self.op('<+>', return_type=Float)(other) + comparator_factory = Comparator + # for reflection ischema_names['vector'] = VECTOR # type: ignore From 1cbee1ac5cee097d1ccb7ff811c18c003653ae75 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Sat, 10 Oct 2026 15:13:57 -0700 Subject: [PATCH 07/35] Fixed CI --- pgvector/sqlalchemy/bit.py | 2 +- pgvector/sqlalchemy/halfvec.py | 2 +- pgvector/sqlalchemy/sparsevec.py | 2 +- pgvector/sqlalchemy/vector.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pgvector/sqlalchemy/bit.py b/pgvector/sqlalchemy/bit.py index 7c861ef..a5af3b0 100644 --- a/pgvector/sqlalchemy/bit.py +++ b/pgvector/sqlalchemy/bit.py @@ -14,7 +14,7 @@ def process_bind_param(self, value: Any, dialect: Any) -> Any: return asyncpg.BitString(value) # type: ignore return value - class Comparator(TypeDecorator.Comparator[Any]): + class Comparator(TypeDecorator.Comparator[Any]): # type: ignore def hamming_distance(self, other: object, /) -> Operators: return self.op('<~>', return_type=Float)(other) diff --git a/pgvector/sqlalchemy/halfvec.py b/pgvector/sqlalchemy/halfvec.py index 9d717c3..d82c5ac 100644 --- a/pgvector/sqlalchemy/halfvec.py +++ b/pgvector/sqlalchemy/halfvec.py @@ -35,7 +35,7 @@ def process(value: Any) -> list[float] | None: return HalfVector._from_db(value) return process - class Comparator(TypeEngine.Comparator[Any]): + class Comparator(TypeEngine.Comparator[Any]): # type: ignore def l2_distance(self, other: object, /) -> Operators: return self.op('<->', return_type=Float)(other) diff --git a/pgvector/sqlalchemy/sparsevec.py b/pgvector/sqlalchemy/sparsevec.py index 9bd1478..5cc910d 100644 --- a/pgvector/sqlalchemy/sparsevec.py +++ b/pgvector/sqlalchemy/sparsevec.py @@ -35,7 +35,7 @@ def process(value: Any) -> SparseVector | None: return SparseVector._from_db(value) return process - class Comparator(TypeEngine.Comparator[Any]): + class Comparator(TypeEngine.Comparator[Any]): # type: ignore def l2_distance(self, other: object, /) -> Operators: return self.op('<->', return_type=Float)(other) diff --git a/pgvector/sqlalchemy/vector.py b/pgvector/sqlalchemy/vector.py index 83acb09..4e7e4b3 100644 --- a/pgvector/sqlalchemy/vector.py +++ b/pgvector/sqlalchemy/vector.py @@ -35,7 +35,7 @@ def process(value: Any) -> list[float] | None: return Vector._from_db(value) return process - class Comparator(TypeEngine.Comparator[Any]): + class Comparator(TypeEngine.Comparator[Any]): # type: ignore def l2_distance(self, other: object, /) -> Operators: return self.op('<->', return_type=Float)(other) From 8cef6e4e867b8d07971da1a3d6170fb6e75e3c40 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Tue, 13 Oct 2026 13:11:57 -0700 Subject: [PATCH 08/35] Fixed equality for HalfVector with signed zeros - resolves #164 Co-authored-by: Zhuo Zhao <193034689+chaduo@users.noreply.github.com> --- CHANGELOG.md | 4 ++++ pgvector/halfvec.py | 2 +- tests/test_half_vector.py | 1 + tests/test_vector.py | 1 + 4 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 666ca29..9561ff5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.5.2 (unreleased) + +- Fixed equality for `HalfVector` with signed zeros + ## 0.5.1 (2026-10-08) - Fixed error with non-canonical SciPy sparse arrays diff --git a/pgvector/halfvec.py b/pgvector/halfvec.py index 1a2c8c7..fa35194 100644 --- a/pgvector/halfvec.py +++ b/pgvector/halfvec.py @@ -37,7 +37,7 @@ def __repr__(self) -> str: def __eq__(self, other: object, /) -> bool: if not isinstance(other, self.__class__): return NotImplemented - return self._value == other._value + return self.to_list() == other.to_list() def dimensions(self) -> int: return len(self._value) diff --git a/tests/test_half_vector.py b/tests/test_half_vector.py index 26c5ac7..56bda26 100644 --- a/tests/test_half_vector.py +++ b/tests/test_half_vector.py @@ -55,6 +55,7 @@ def test_equality(self) -> None: assert HalfVector([1, 2, 3]) == HalfVector([1, 2, 3]) assert HalfVector([1, 2, 3]) != HalfVector([1, 2, 4]) assert HalfVector([1, 2, 3]) != 1 + assert HalfVector([0.0]) == HalfVector([-0.0]) def test_dimensions(self) -> None: assert HalfVector([1, 2, 3]).dimensions() == 3 diff --git a/tests/test_vector.py b/tests/test_vector.py index a08370e..c5b75d0 100644 --- a/tests/test_vector.py +++ b/tests/test_vector.py @@ -55,6 +55,7 @@ def test_equality(self) -> None: assert Vector([1, 2, 3]) == Vector([1, 2, 3]) assert Vector([1, 2, 3]) != Vector([1, 2, 4]) assert Vector([1, 2, 3]) != 1 + assert Vector([0.0]) == Vector([-0.0]) def test_dimensions(self) -> None: assert Vector([1, 2, 3]).dimensions() == 3 From 2915a599ac1a22876fe5d05fdb55bc40feadaa36 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Wed, 14 Oct 2026 14:16:26 -0700 Subject: [PATCH 09/35] Fixed error with Bit constructor and empty strings - fixes #165 and resolves #166 Co-authored-by: Zaakir Hussain --- CHANGELOG.md | 1 + pgvector/bit.py | 5 ++++- tests/test_bit.py | 3 +++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9561ff5..e1c1f48 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## 0.5.2 (unreleased) +- Fixed error with `Bit` constructor and empty strings - Fixed equality for `HalfVector` with signed zeros ## 0.5.1 (2026-10-08) diff --git a/pgvector/bit.py b/pgvector/bit.py index a157b30..098ebb2 100644 --- a/pgvector/bit.py +++ b/pgvector/bit.py @@ -34,7 +34,10 @@ def __init__( value += '0' * (8 - (length % 8)) self._length = length - self._data = int(value, 2).to_bytes(len(value) // 8, byteorder='big') + if length > 0: + self._data = int(value, 2).to_bytes(len(value) // 8, byteorder='big') + else: + self._data = b'' elif is_ndarray(value): import numpy as np diff --git a/tests/test_bit.py b/tests/test_bit.py index ed13374..2714ac2 100644 --- a/tests/test_bit.py +++ b/tests/test_bit.py @@ -27,6 +27,9 @@ def test_list_list(self) -> None: def test_str(self) -> None: assert Bit('101').to_list() == [True, False, True] + def test_str_empty(self) -> None: + assert Bit('').to_list() == [] + def test_str_two(self) -> None: with pytest.raises(ValueError) as error: Bit('201') From 7329d8a3973cfe9390296f12b471adcd6070e2da Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Wed, 14 Oct 2026 14:22:51 -0700 Subject: [PATCH 10/35] Added more tests for zero length bit [skip ci] --- tests/test_bit.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/test_bit.py b/tests/test_bit.py index 2714ac2..6487348 100644 --- a/tests/test_bit.py +++ b/tests/test_bit.py @@ -9,6 +9,9 @@ class TestBit: def test_list(self) -> None: assert Bit([True, False, True]).to_list() == [True, False, True] + def test_list_empty(self) -> None: + assert Bit([]).to_list() == [] + def test_list_none(self) -> None: with pytest.raises(ValueError) as error: Bit([True, None, True]) # type: ignore @@ -39,6 +42,9 @@ def test_bytes(self) -> None: assert Bit(b'\xff\x00\xf0').to_text() == '111111110000000011110000' assert Bit(b'\xfe\x07\x00').to_text() == '111111100000011100000000' + def test_bytes_empty(self) -> None: + assert Bit(b'').to_text() == '' + def test_ndarray(self) -> None: if np is None: pytest.skip('NumPy required') From 4003693d0f398cf87068bcac404a0d082291953a Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Fri, 16 Oct 2026 13:44:16 -0700 Subject: [PATCH 11/35] Prepared validation for SparseVector indexes - resolves #167 [skip ci] Co-authored-by: AryanRG --- pgvector/sparsevec.py | 8 ++++++++ tests/test_sparse_vector.py | 10 ++++++++++ 2 files changed, 18 insertions(+) diff --git a/pgvector/sparsevec.py b/pgvector/sparsevec.py index 0d3434c..bf8f3b4 100644 --- a/pgvector/sparsevec.py +++ b/pgvector/sparsevec.py @@ -101,6 +101,8 @@ def _from_dict(self, d: dict[int, float], dim: int) -> None: self._dim = int(dim) self._indices = [int(v[0]) for v in elements] self._values = [float(v[1]) for v in elements] + # TODO uncomment in 0.6.0 + # self._validate() def _from_sparse(self, arr: sparray | spmatrix, /) -> None: value: coo_array | coo_matrix = arr.tocoo(copy=False) # type: ignore @@ -138,6 +140,10 @@ def _from_dense(self, value: list[float] | ndarray, /) -> None: self._indices = [i for i, v in enumerate(value) if v != 0] self._values = [float(value[i]) for i in self._indices] + def _validate(self): + if any(i < 0 or i >= self._dim for i in self._indices): + raise ValueError('sparsevec index out of bounds') + @classmethod def from_text(cls, value: str, /) -> SparseVector: elements, dim = value.split('/', 2) @@ -171,6 +177,8 @@ def _from_parts(cls, dim: int, indices: list[int], values: list[float], /) -> Sp vec._dim = dim vec._indices = indices vec._values = values + # TODO uncomment in 0.6.0 + # vec._validate() return vec @classmethod diff --git a/tests/test_sparse_vector.py b/tests/test_sparse_vector.py index 262e23a..78ca985 100644 --- a/tests/test_sparse_vector.py +++ b/tests/test_sparse_vector.py @@ -45,6 +45,16 @@ def test_dict(self) -> None: def test_dict_empty(self) -> None: assert SparseVector({}, 0).to_list() == [] + # TODO uncomment in 0.6.0 + # def test_dict_negative_index(self) -> None: + # with pytest.raises(ValueError, match='sparsevec index out of bounds'): + # SparseVector({-1: 1}, 3) + + # TODO uncomment in 0.6.0 + # def test_dict_higher_index(self) -> None: + # with pytest.raises(ValueError, match='sparsevec index out of bounds'): + # SparseVector({3: 1}, 3) + def test_dict_no_dimensions(self) -> None: with pytest.raises(ValueError) as error: SparseVector({0: 1, 2: 2, 4: 3}) # type: ignore From 6ce28526a13590884e87f9613bbc058860e9c708 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Fri, 16 Oct 2026 13:46:40 -0700 Subject: [PATCH 12/35] Prepared validation for SparseVector dimensions [skip ci] --- pgvector/sparsevec.py | 3 +++ tests/test_sparse_vector.py | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/pgvector/sparsevec.py b/pgvector/sparsevec.py index bf8f3b4..335a9ad 100644 --- a/pgvector/sparsevec.py +++ b/pgvector/sparsevec.py @@ -141,6 +141,9 @@ def _from_dense(self, value: list[float] | ndarray, /) -> None: self._values = [float(value[i]) for i in self._indices] def _validate(self): + if self._dim < 0: + raise ValueError('sparsevec cannot have negative dimensions') + if any(i < 0 or i >= self._dim for i in self._indices): raise ValueError('sparsevec index out of bounds') diff --git a/tests/test_sparse_vector.py b/tests/test_sparse_vector.py index 78ca985..9d64c3d 100644 --- a/tests/test_sparse_vector.py +++ b/tests/test_sparse_vector.py @@ -55,6 +55,11 @@ def test_dict_empty(self) -> None: # with pytest.raises(ValueError, match='sparsevec index out of bounds'): # SparseVector({3: 1}, 3) + # TODO uncomment in 0.6.0 + # def test_dict_negative_dimensions(self) -> None: + # with pytest.raises(ValueError, match='sparsevec cannot have negative dimensions'): + # SparseVector({}, -1) + def test_dict_no_dimensions(self) -> None: with pytest.raises(ValueError) as error: SparseVector({0: 1, 2: 2, 4: 3}) # type: ignore From 8be9673245133041bb696c1071b153a20f88585b Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Fri, 16 Oct 2026 22:16:24 -0700 Subject: [PATCH 13/35] Added typing [skip ci] --- pgvector/sparsevec.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgvector/sparsevec.py b/pgvector/sparsevec.py index 335a9ad..6a477a9 100644 --- a/pgvector/sparsevec.py +++ b/pgvector/sparsevec.py @@ -140,7 +140,7 @@ def _from_dense(self, value: list[float] | ndarray, /) -> None: self._indices = [i for i, v in enumerate(value) if v != 0] self._values = [float(value[i]) for i in self._indices] - def _validate(self): + def _validate(self) -> None: if self._dim < 0: raise ValueError('sparsevec cannot have negative dimensions') From 063995bcfaeb58aa86b3ec6306987f09cce18f1b Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Fri, 16 Oct 2026 22:24:54 -0700 Subject: [PATCH 14/35] Prepared validation for SparseVector order [skip ci] --- pgvector/sparsevec.py | 6 ++++++ tests/test_sparse_vector.py | 10 ++++++++++ 2 files changed, 16 insertions(+) diff --git a/pgvector/sparsevec.py b/pgvector/sparsevec.py index 6a477a9..c3ac7e5 100644 --- a/pgvector/sparsevec.py +++ b/pgvector/sparsevec.py @@ -147,6 +147,11 @@ def _validate(self) -> None: if any(i < 0 or i >= self._dim for i in self._indices): raise ValueError('sparsevec index out of bounds') + def _validate_sorted(self) -> None: + for i in range(len(self._indices) - 1): + if self._indices[i] >= self._indices[i + 1]: + raise ValueError('sparsevec indices must be in ascending order') + @classmethod def from_text(cls, value: str, /) -> SparseVector: elements, dim = value.split('/', 2) @@ -182,6 +187,7 @@ def _from_parts(cls, dim: int, indices: list[int], values: list[float], /) -> Sp vec._values = values # TODO uncomment in 0.6.0 # vec._validate() + # vec._validate_sorted() return vec @classmethod diff --git a/tests/test_sparse_vector.py b/tests/test_sparse_vector.py index 9d64c3d..2b8fd1f 100644 --- a/tests/test_sparse_vector.py +++ b/tests/test_sparse_vector.py @@ -169,6 +169,16 @@ def test_from_text(self) -> None: assert np.array_equal(vec.to_numpy(), [1.5, 0, 2, 0, 3, 0]) assert vec.to_text() == '{1:1.5,3:2.0,5:3.0}/6' + # TODO uncomment in 0.6.0 + # def test_from_text_unsorted(self) -> None: + # with pytest.raises(ValueError, match='sparsevec indices must be in ascending order'): + # SparseVector.from_text('{2:1,1:1}/3') + + # TODO uncomment in 0.6.0 + # def test_from_text_duplicate(self) -> None: + # with pytest.raises(ValueError, match='sparsevec indices must be in ascending order'): + # SparseVector.from_text('{2:1,2:1}/3') + def test_from_binary(self) -> None: data = pack('>iii3i3f', 6, 3, 0, 0, 2, 4, 1.5, 2, 3) vec = SparseVector.from_binary(data) From b5d9b7084024a26d4d7b0dda92280a23e7f6d518 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Fri, 16 Oct 2026 22:33:07 -0700 Subject: [PATCH 15/35] Improved _from_dense method --- pgvector/sparsevec.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/pgvector/sparsevec.py b/pgvector/sparsevec.py index c3ac7e5..76e1356 100644 --- a/pgvector/sparsevec.py +++ b/pgvector/sparsevec.py @@ -137,8 +137,13 @@ def _from_sparse(self, arr: sparray | spmatrix, /) -> None: def _from_dense(self, value: list[float] | ndarray, /) -> None: self._dim = len(value) - self._indices = [i for i, v in enumerate(value) if v != 0] - self._values = [float(value[i]) for i in self._indices] + self._indices = [] + self._values = [] + for i, v in enumerate(value): + v = float(v) + if v != 0: + self._indices.append(i) + self._values.append(v) def _validate(self) -> None: if self._dim < 0: From 97e9fefe23fdc787e3bea96f06b1d2686f8b6d5d Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Wed, 21 Oct 2026 13:01:51 -0700 Subject: [PATCH 16/35] Fixed memory leak with register_vector and register_vector_async for Psycopg 3 - fixes #168 --- CHANGELOG.md | 1 + pgvector/psycopg/bit.py | 15 +++++++++++++-- pgvector/psycopg/halfvec.py | 15 +++++++++++++-- pgvector/psycopg/sparsevec.py | 15 +++++++++++++-- pgvector/psycopg/vector.py | 15 +++++++++++++-- 5 files changed, 53 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e1c1f48..f033cf1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## 0.5.2 (unreleased) +- Fixed memory leak with `register_vector` and `register_vector_async` for Psycopg 3 - Fixed error with `Bit` constructor and empty strings - Fixed equality for `HalfVector` with signed zeros diff --git a/pgvector/psycopg/bit.py b/pgvector/psycopg/bit.py index f899497..d00c019 100644 --- a/pgvector/psycopg/bit.py +++ b/pgvector/psycopg/bit.py @@ -1,3 +1,4 @@ +from functools import cache from psycopg import BaseConnection from psycopg.abc import Buffer from psycopg.adapt import Dumper @@ -26,9 +27,19 @@ def register_bit_info(context: BaseConnection[Any], info: TypeInfo | None, /) -> info.register(context) # add oid to anonymous class for set_types - text_dumper = type('', (BitDumper,), {'oid': info.oid}) - binary_dumper = type('', (BitBinaryDumper,), {'oid': info.oid}) + text_dumper = bit_text_dumper(info.oid) + binary_dumper = bit_binary_dumper(info.oid) adapters = context.adapters adapters.register_dumper(Bit, text_dumper) adapters.register_dumper(Bit, binary_dumper) + + +@cache +def bit_text_dumper(oid: int): + return type('', (BitDumper,), {'oid': oid}) + + +@cache +def bit_binary_dumper(oid: int): + return type('', (BitBinaryDumper,), {'oid': oid}) diff --git a/pgvector/psycopg/halfvec.py b/pgvector/psycopg/halfvec.py index 9ed0453..4ca14bb 100644 --- a/pgvector/psycopg/halfvec.py +++ b/pgvector/psycopg/halfvec.py @@ -1,3 +1,4 @@ +from functools import cache from psycopg import BaseConnection from psycopg.abc import Buffer from psycopg.adapt import Loader, Dumper @@ -41,11 +42,21 @@ def register_halfvec_info(context: BaseConnection[Any], info: TypeInfo, /) -> No info.register(context) # add oid to anonymous class for set_types - text_dumper = type('', (HalfVectorDumper,), {'oid': info.oid}) - binary_dumper = type('', (HalfVectorBinaryDumper,), {'oid': info.oid}) + text_dumper = halfvec_text_dumper(info.oid) + binary_dumper = halfvec_binary_dumper(info.oid) adapters = context.adapters adapters.register_dumper(HalfVector, text_dumper) adapters.register_dumper(HalfVector, binary_dumper) adapters.register_loader(info.oid, HalfVectorLoader) adapters.register_loader(info.oid, HalfVectorBinaryLoader) + + +@cache +def halfvec_text_dumper(oid: int): + return type('', (HalfVectorDumper,), {'oid': oid}) + + +@cache +def halfvec_binary_dumper(oid: int): + return type('', (HalfVectorBinaryDumper,), {'oid': oid}) diff --git a/pgvector/psycopg/sparsevec.py b/pgvector/psycopg/sparsevec.py index 57fd19a..2796c94 100644 --- a/pgvector/psycopg/sparsevec.py +++ b/pgvector/psycopg/sparsevec.py @@ -1,3 +1,4 @@ +from functools import cache from psycopg import BaseConnection from psycopg.abc import Buffer from psycopg.adapt import Loader, Dumper @@ -41,11 +42,21 @@ def register_sparsevec_info(context: BaseConnection[Any], info: TypeInfo, /) -> info.register(context) # add oid to anonymous class for set_types - text_dumper = type('', (SparseVectorDumper,), {'oid': info.oid}) - binary_dumper = type('', (SparseVectorBinaryDumper,), {'oid': info.oid}) + text_dumper = sparsevec_text_dumper(info.oid) + binary_dumper = sparsevec_binary_dumper(info.oid) adapters = context.adapters adapters.register_dumper(SparseVector, text_dumper) adapters.register_dumper(SparseVector, binary_dumper) adapters.register_loader(info.oid, SparseVectorLoader) adapters.register_loader(info.oid, SparseVectorBinaryLoader) + + +@cache +def sparsevec_text_dumper(oid: int): + return type('', (SparseVectorDumper,), {'oid': oid}) + + +@cache +def sparsevec_binary_dumper(oid: int): + return type('', (SparseVectorBinaryDumper,), {'oid': oid}) diff --git a/pgvector/psycopg/vector.py b/pgvector/psycopg/vector.py index 7ef16b0..16df1c9 100644 --- a/pgvector/psycopg/vector.py +++ b/pgvector/psycopg/vector.py @@ -1,4 +1,5 @@ from __future__ import annotations +from functools import cache import psycopg from psycopg import BaseConnection from psycopg.abc import Buffer @@ -50,8 +51,8 @@ def register_vector_info(context: BaseConnection[Any], info: TypeInfo | None, /) info.register(context) # add oid to anonymous class for set_types - text_dumper = type('', (VectorDumper,), {'oid': info.oid}) - binary_dumper = type('', (VectorBinaryDumper,), {'oid': info.oid}) + text_dumper = vector_text_dumper(info.oid) + binary_dumper = vector_binary_dumper(info.oid) adapters = context.adapters adapters.register_dumper(Vector, text_dumper) @@ -60,3 +61,13 @@ def register_vector_info(context: BaseConnection[Any], info: TypeInfo | None, /) adapters.register_dumper('numpy.ndarray', binary_dumper) adapters.register_loader(info.oid, VectorLoader) adapters.register_loader(info.oid, VectorBinaryLoader) + + +@cache +def vector_text_dumper(oid: int): + return type('', (VectorDumper,), {'oid': oid}) + + +@cache +def vector_binary_dumper(oid: int): + return type('', (VectorBinaryDumper,), {'oid': oid}) From e0f3ac37b8f05216cd02a3e4cdd502bcbdb0539d Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Wed, 21 Oct 2026 13:23:29 -0700 Subject: [PATCH 17/35] Added missing typing --- pgvector/psycopg/bit.py | 4 ++-- pgvector/psycopg/halfvec.py | 4 ++-- pgvector/psycopg/sparsevec.py | 4 ++-- pgvector/psycopg/vector.py | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pgvector/psycopg/bit.py b/pgvector/psycopg/bit.py index d00c019..4cb502f 100644 --- a/pgvector/psycopg/bit.py +++ b/pgvector/psycopg/bit.py @@ -36,10 +36,10 @@ def register_bit_info(context: BaseConnection[Any], info: TypeInfo | None, /) -> @cache -def bit_text_dumper(oid: int): +def bit_text_dumper(oid: int) -> type[BitDumper]: return type('', (BitDumper,), {'oid': oid}) @cache -def bit_binary_dumper(oid: int): +def bit_binary_dumper(oid: int) -> type[BitBinaryDumper]: return type('', (BitBinaryDumper,), {'oid': oid}) diff --git a/pgvector/psycopg/halfvec.py b/pgvector/psycopg/halfvec.py index 4ca14bb..05e7994 100644 --- a/pgvector/psycopg/halfvec.py +++ b/pgvector/psycopg/halfvec.py @@ -53,10 +53,10 @@ def register_halfvec_info(context: BaseConnection[Any], info: TypeInfo, /) -> No @cache -def halfvec_text_dumper(oid: int): +def halfvec_text_dumper(oid: int) -> type[HalfVectorDumper]: return type('', (HalfVectorDumper,), {'oid': oid}) @cache -def halfvec_binary_dumper(oid: int): +def halfvec_binary_dumper(oid: int) -> type[HalfVectorBinaryDumper]: return type('', (HalfVectorBinaryDumper,), {'oid': oid}) diff --git a/pgvector/psycopg/sparsevec.py b/pgvector/psycopg/sparsevec.py index 2796c94..0c953bf 100644 --- a/pgvector/psycopg/sparsevec.py +++ b/pgvector/psycopg/sparsevec.py @@ -53,10 +53,10 @@ def register_sparsevec_info(context: BaseConnection[Any], info: TypeInfo, /) -> @cache -def sparsevec_text_dumper(oid: int): +def sparsevec_text_dumper(oid: int) -> type[SparseVectorDumper]: return type('', (SparseVectorDumper,), {'oid': oid}) @cache -def sparsevec_binary_dumper(oid: int): +def sparsevec_binary_dumper(oid: int) -> type[SparseVectorBinaryDumper]: return type('', (SparseVectorBinaryDumper,), {'oid': oid}) diff --git a/pgvector/psycopg/vector.py b/pgvector/psycopg/vector.py index 16df1c9..f4b66e4 100644 --- a/pgvector/psycopg/vector.py +++ b/pgvector/psycopg/vector.py @@ -64,10 +64,10 @@ def register_vector_info(context: BaseConnection[Any], info: TypeInfo | None, /) @cache -def vector_text_dumper(oid: int): +def vector_text_dumper(oid: int) -> type[VectorDumper]: return type('', (VectorDumper,), {'oid': oid}) @cache -def vector_binary_dumper(oid: int): +def vector_binary_dumper(oid: int) -> type[VectorBinaryDumper]: return type('', (VectorBinaryDumper,), {'oid': oid}) From a75513769f8cc5a696bab2949a81294004600a4d Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Tue, 27 Oct 2026 11:50:26 -0700 Subject: [PATCH 18/35] Fixed Bit.from_binary with negative length - resolves #173 Co-authored-by: beyondsundial <67931863+beyondsundial@users.noreply.github.com> --- pgvector/bit.py | 2 +- tests/test_bit.py | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/pgvector/bit.py b/pgvector/bit.py index 098ebb2..a9370e4 100644 --- a/pgvector/bit.py +++ b/pgvector/bit.py @@ -88,7 +88,7 @@ def from_binary(cls, value: bytes | bytearray | memoryview, /) -> Bit: length, = unpack_from('>i', value) data = memoryview(value)[4:].tobytes() - if len(data) != (length + 7) // 8: + if length < 0 or len(data) != (length + 7) // 8: raise ValueError('invalid length') bit = cls.__new__(cls) diff --git a/tests/test_bit.py b/tests/test_bit.py index 6487348..caeb33f 100644 --- a/tests/test_bit.py +++ b/tests/test_bit.py @@ -110,3 +110,8 @@ def test_from_binary(self) -> None: if np is not None: assert np.array_equal(vec.to_numpy(), [True, False, True]) assert vec.to_binary() == data + + def test_from_binary_negative_length(self) -> None: + with pytest.raises(ValueError) as error: + Bit.from_binary(pack('>i', -1)) + assert str(error.value) == 'invalid length' From d702a70e05b1a4396266e37caf86766d0a66d7d4 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Tue, 27 Oct 2026 17:22:06 -0700 Subject: [PATCH 19/35] Improved tests for from_binary methods --- tests/test_bit.py | 18 +++++++++++++----- tests/test_half_vector.py | 17 +++++++++++++++-- tests/test_sparse_vector.py | 17 +++++++++++++++-- tests/test_vector.py | 17 +++++++++++++++-- 4 files changed, 58 insertions(+), 11 deletions(-) diff --git a/tests/test_bit.py b/tests/test_bit.py index caeb33f..2125fbf 100644 --- a/tests/test_bit.py +++ b/tests/test_bit.py @@ -1,7 +1,7 @@ from pgvector import Bit import pytest import random -from struct import pack +import struct from .conftest import numpy as np @@ -104,14 +104,22 @@ def test_from_text(self) -> None: assert vec.to_text() == '101' def test_from_binary(self) -> None: - data = pack('>iB', 3, 5 << 5) + data = struct.pack('>iB', 3, 5 << 5) vec = Bit.from_binary(data) assert vec.to_list() == [True, False, True] if np is not None: assert np.array_equal(vec.to_numpy(), [True, False, True]) assert vec.to_binary() == data + def test_from_binary_zero_length(self) -> None: + # TODO change to ValueError('invalid length') in 0.6.0 + with pytest.raises(struct.error, match='unpack_from requires a buffer of at least 4 bytes'): + Bit.from_binary(b'') + def test_from_binary_negative_length(self) -> None: - with pytest.raises(ValueError) as error: - Bit.from_binary(pack('>i', -1)) - assert str(error.value) == 'invalid length' + with pytest.raises(ValueError, match='invalid length'): + Bit.from_binary(struct.pack('>i', -1)) + + def test_from_binary_invalid_length(self) -> None: + with pytest.raises(ValueError, match='invalid length'): + Bit.from_binary(struct.pack('>i', 1)) diff --git a/tests/test_half_vector.py b/tests/test_half_vector.py index 56bda26..6932c48 100644 --- a/tests/test_half_vector.py +++ b/tests/test_half_vector.py @@ -1,6 +1,6 @@ from pgvector import HalfVector import pytest -from struct import pack +import struct from .conftest import numpy as np @@ -68,9 +68,22 @@ def test_from_text(self) -> None: assert vec.to_text() == '[1.5,2.0,3.0]' def test_from_binary(self) -> None: - data = pack('>HH3e', 3, 0, 1.5, 2, 3) + data = struct.pack('>HH3e', 3, 0, 1.5, 2, 3) vec = HalfVector.from_binary(data) assert vec.to_list() == [1.5, 2, 3] if np is not None: assert np.array_equal(vec.to_numpy(), [1.5, 2, 3]) assert vec.to_binary() == data + + def test_from_binary_zero_length(self) -> None: + # TODO change to ValueError('invalid length') in 0.6.0 + with pytest.raises(struct.error, match='unpack_from requires a buffer of at least 4 bytes'): + HalfVector.from_binary(b'') + + def test_from_binary_negative_dimensions(self) -> None: + with pytest.raises(ValueError, match='invalid length'): + HalfVector.from_binary(struct.pack('>hh', -1, 0)) + + def test_from_binary_non_zero_unused(self) -> None: + with pytest.raises(ValueError, match='expected unused to be 0'): + HalfVector.from_binary(struct.pack('>hh', 0, 1)) diff --git a/tests/test_sparse_vector.py b/tests/test_sparse_vector.py index 2b8fd1f..4609e2e 100644 --- a/tests/test_sparse_vector.py +++ b/tests/test_sparse_vector.py @@ -1,6 +1,6 @@ from pgvector import SparseVector import pytest -from struct import pack +import struct from .conftest import numpy as np try: @@ -180,7 +180,7 @@ def test_from_text(self) -> None: # SparseVector.from_text('{2:1,2:1}/3') def test_from_binary(self) -> None: - data = pack('>iii3i3f', 6, 3, 0, 0, 2, 4, 1.5, 2, 3) + data = struct.pack('>iii3i3f', 6, 3, 0, 0, 2, 4, 1.5, 2, 3) vec = SparseVector.from_binary(data) assert vec.dimensions() == 6 assert vec.indices() == [0, 2, 4] @@ -189,3 +189,16 @@ def test_from_binary(self) -> None: if np is not None: assert np.array_equal(vec.to_numpy(), [1.5, 0, 2, 0, 3, 0]) assert vec.to_binary() == data + + def test_from_binary_zero_length(self) -> None: + # TODO change to ValueError('invalid length') in 0.6.0 + with pytest.raises(struct.error, match='unpack_from requires a buffer of at least 12 bytes'): + SparseVector.from_binary(b'') + + def test_from_binary_negative_nnz(self) -> None: + with pytest.raises(ValueError, match='invalid length'): + SparseVector.from_binary(struct.pack('>iii', 0, -1, 0)) + + def test_from_binary_non_zero_unused(self) -> None: + with pytest.raises(ValueError, match='expected unused to be 0'): + SparseVector.from_binary(struct.pack('>iii', 0, 0, 1)) diff --git a/tests/test_vector.py b/tests/test_vector.py index c5b75d0..23f1e0d 100644 --- a/tests/test_vector.py +++ b/tests/test_vector.py @@ -1,6 +1,6 @@ from pgvector import Vector import pytest -from struct import pack +import struct from .conftest import numpy as np @@ -68,9 +68,22 @@ def test_from_text(self) -> None: assert vec.to_text() == '[1.5,2.0,3.0]' def test_from_binary(self) -> None: - data = pack('>HH3f', 3, 0, 1.5, 2, 3) + data = struct.pack('>hh3f', 3, 0, 1.5, 2, 3) vec = Vector.from_binary(data) assert vec.to_list() == [1.5, 2, 3] if np is not None: assert np.array_equal(vec.to_numpy(), [1.5, 2, 3]) assert vec.to_binary() == data + + def test_from_binary_zero_length(self) -> None: + # TODO change to ValueError('invalid length') in 0.6.0 + with pytest.raises(struct.error, match='unpack_from requires a buffer of at least 4 bytes'): + Vector.from_binary(b'') + + def test_from_binary_negative_dimensions(self) -> None: + with pytest.raises(ValueError, match='invalid length'): + Vector.from_binary(struct.pack('>hh', -1, 0)) + + def test_from_binary_non_zero_unused(self) -> None: + with pytest.raises(ValueError, match='expected unused to be 0'): + Vector.from_binary(struct.pack('>hh', 0, 1)) From d2976a0f3f859d114c3385fbbb758dd125feefaa Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Tue, 27 Oct 2026 17:26:35 -0700 Subject: [PATCH 20/35] Improved format characters [skip ci] --- pgvector/halfvec.py | 3 ++- pgvector/vector.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/pgvector/halfvec.py b/pgvector/halfvec.py index fa35194..8658b09 100644 --- a/pgvector/halfvec.py +++ b/pgvector/halfvec.py @@ -59,6 +59,7 @@ def to_binary(self) -> bytes: else: value = array('H', self._value) value.byteswap() + # TODO change to >hh in 0.6.0 return struct.pack('>HH', len(value), 0) + memoryview(value) @classmethod @@ -67,7 +68,7 @@ def from_text(cls, value: str, /) -> HalfVector: @classmethod def from_binary(cls, value: bytes | bytearray | memoryview, /) -> HalfVector: - dim, unused = struct.unpack_from('>HH', value) + dim, unused = struct.unpack_from('>hh', value) data = memoryview(value)[4:] if len(data) != 2 * dim: diff --git a/pgvector/vector.py b/pgvector/vector.py index d357113..10f70fa 100644 --- a/pgvector/vector.py +++ b/pgvector/vector.py @@ -57,6 +57,7 @@ def to_binary(self) -> bytes: else: value = array('f', self._value) value.byteswap() + # TODO change to >hh in 0.6.0 return struct.pack('>HH', len(value), 0) + memoryview(value) @classmethod @@ -65,7 +66,7 @@ def from_text(cls, value: str, /) -> Vector: @classmethod def from_binary(cls, value: bytes | bytearray | memoryview, /) -> Vector: - dim, unused = struct.unpack_from('>HH', value) + dim, unused = struct.unpack_from('>hh', value) data = memoryview(value)[4:] if len(data) != 4 * dim: From 47a6300e764157f01af582956c6f93eb0e15bbac Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Tue, 27 Oct 2026 17:35:34 -0700 Subject: [PATCH 21/35] Improved tests for from_binary methods [skip ci] --- tests/test_bit.py | 8 ++++---- tests/test_half_vector.py | 4 ++++ tests/test_sparse_vector.py | 4 ++++ tests/test_vector.py | 4 ++++ 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/tests/test_bit.py b/tests/test_bit.py index 2125fbf..6823c46 100644 --- a/tests/test_bit.py +++ b/tests/test_bit.py @@ -116,10 +116,10 @@ def test_from_binary_zero_length(self) -> None: with pytest.raises(struct.error, match='unpack_from requires a buffer of at least 4 bytes'): Bit.from_binary(b'') - def test_from_binary_negative_length(self) -> None: - with pytest.raises(ValueError, match='invalid length'): - Bit.from_binary(struct.pack('>i', -1)) - def test_from_binary_invalid_length(self) -> None: with pytest.raises(ValueError, match='invalid length'): Bit.from_binary(struct.pack('>i', 1)) + + def test_from_binary_negative_length(self) -> None: + with pytest.raises(ValueError, match='invalid length'): + Bit.from_binary(struct.pack('>i', -1)) diff --git a/tests/test_half_vector.py b/tests/test_half_vector.py index 6932c48..6d9146f 100644 --- a/tests/test_half_vector.py +++ b/tests/test_half_vector.py @@ -80,6 +80,10 @@ def test_from_binary_zero_length(self) -> None: with pytest.raises(struct.error, match='unpack_from requires a buffer of at least 4 bytes'): HalfVector.from_binary(b'') + def test_from_binary_invalid_dimensions(self) -> None: + with pytest.raises(ValueError, match='invalid length'): + HalfVector.from_binary(struct.pack('>hh', 1, 0)) + def test_from_binary_negative_dimensions(self) -> None: with pytest.raises(ValueError, match='invalid length'): HalfVector.from_binary(struct.pack('>hh', -1, 0)) diff --git a/tests/test_sparse_vector.py b/tests/test_sparse_vector.py index 4609e2e..b05ccee 100644 --- a/tests/test_sparse_vector.py +++ b/tests/test_sparse_vector.py @@ -195,6 +195,10 @@ def test_from_binary_zero_length(self) -> None: with pytest.raises(struct.error, match='unpack_from requires a buffer of at least 12 bytes'): SparseVector.from_binary(b'') + def test_from_binary_invalid_nnz(self) -> None: + with pytest.raises(ValueError, match='invalid length'): + SparseVector.from_binary(struct.pack('>iii', 1, 1, 0)) + def test_from_binary_negative_nnz(self) -> None: with pytest.raises(ValueError, match='invalid length'): SparseVector.from_binary(struct.pack('>iii', 0, -1, 0)) diff --git a/tests/test_vector.py b/tests/test_vector.py index 23f1e0d..00e497e 100644 --- a/tests/test_vector.py +++ b/tests/test_vector.py @@ -80,6 +80,10 @@ def test_from_binary_zero_length(self) -> None: with pytest.raises(struct.error, match='unpack_from requires a buffer of at least 4 bytes'): Vector.from_binary(b'') + def test_from_binary_invalid_dimensions(self) -> None: + with pytest.raises(ValueError, match='invalid length'): + Vector.from_binary(struct.pack('>hh', 1, 0)) + def test_from_binary_negative_dimensions(self) -> None: with pytest.raises(ValueError, match='invalid length'): Vector.from_binary(struct.pack('>hh', -1, 0)) From 845ee44437086c869e1d7a181dcd924b91b513de Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Tue, 27 Oct 2026 17:55:25 -0700 Subject: [PATCH 22/35] Improved tests for from_binary methods [skip ci] --- tests/test_bit.py | 8 ++++++-- tests/test_half_vector.py | 8 ++++++-- tests/test_sparse_vector.py | 8 ++++++-- tests/test_vector.py | 8 ++++++-- 4 files changed, 24 insertions(+), 8 deletions(-) diff --git a/tests/test_bit.py b/tests/test_bit.py index 6823c46..a7df304 100644 --- a/tests/test_bit.py +++ b/tests/test_bit.py @@ -116,9 +116,13 @@ def test_from_binary_zero_length(self) -> None: with pytest.raises(struct.error, match='unpack_from requires a buffer of at least 4 bytes'): Bit.from_binary(b'') - def test_from_binary_invalid_length(self) -> None: + def test_from_binary_low_length(self) -> None: with pytest.raises(ValueError, match='invalid length'): - Bit.from_binary(struct.pack('>i', 1)) + Bit.from_binary(struct.pack('>iBB', 8, 0, 0)) + + def test_from_binary_high_length(self) -> None: + with pytest.raises(ValueError, match='invalid length'): + Bit.from_binary(struct.pack('>iBB', 17, 0, 0)) def test_from_binary_negative_length(self) -> None: with pytest.raises(ValueError, match='invalid length'): diff --git a/tests/test_half_vector.py b/tests/test_half_vector.py index 6d9146f..032b923 100644 --- a/tests/test_half_vector.py +++ b/tests/test_half_vector.py @@ -80,9 +80,13 @@ def test_from_binary_zero_length(self) -> None: with pytest.raises(struct.error, match='unpack_from requires a buffer of at least 4 bytes'): HalfVector.from_binary(b'') - def test_from_binary_invalid_dimensions(self) -> None: + def test_from_binary_low_dimensions(self) -> None: with pytest.raises(ValueError, match='invalid length'): - HalfVector.from_binary(struct.pack('>hh', 1, 0)) + HalfVector.from_binary(struct.pack('>hhee', 1, 0, 1, 2)) + + def test_from_binary_high_dimensions(self) -> None: + with pytest.raises(ValueError, match='invalid length'): + HalfVector.from_binary(struct.pack('>hhee', 3, 0, 1, 2)) def test_from_binary_negative_dimensions(self) -> None: with pytest.raises(ValueError, match='invalid length'): diff --git a/tests/test_sparse_vector.py b/tests/test_sparse_vector.py index b05ccee..e1be6b7 100644 --- a/tests/test_sparse_vector.py +++ b/tests/test_sparse_vector.py @@ -195,9 +195,13 @@ def test_from_binary_zero_length(self) -> None: with pytest.raises(struct.error, match='unpack_from requires a buffer of at least 12 bytes'): SparseVector.from_binary(b'') - def test_from_binary_invalid_nnz(self) -> None: + def test_from_binary_low_nnz(self) -> None: with pytest.raises(ValueError, match='invalid length'): - SparseVector.from_binary(struct.pack('>iii', 1, 1, 0)) + SparseVector.from_binary(struct.pack('>iii2i2f', 3, 1, 0, 0, 1, 1, 2)) + + def test_from_binary_high_nnz(self) -> None: + with pytest.raises(ValueError, match='invalid length'): + SparseVector.from_binary(struct.pack('>iii2i2f', 3, 3, 0, 0, 1, 1, 2)) def test_from_binary_negative_nnz(self) -> None: with pytest.raises(ValueError, match='invalid length'): diff --git a/tests/test_vector.py b/tests/test_vector.py index 00e497e..544b66a 100644 --- a/tests/test_vector.py +++ b/tests/test_vector.py @@ -80,9 +80,13 @@ def test_from_binary_zero_length(self) -> None: with pytest.raises(struct.error, match='unpack_from requires a buffer of at least 4 bytes'): Vector.from_binary(b'') - def test_from_binary_invalid_dimensions(self) -> None: + def test_from_binary_low_dimensions(self) -> None: with pytest.raises(ValueError, match='invalid length'): - Vector.from_binary(struct.pack('>hh', 1, 0)) + Vector.from_binary(struct.pack('>hhff', 1, 0, 1, 2)) + + def test_from_binary_high_dimensions(self) -> None: + with pytest.raises(ValueError, match='invalid length'): + Vector.from_binary(struct.pack('>hhff', 3, 0, 1, 2)) def test_from_binary_negative_dimensions(self) -> None: with pytest.raises(ValueError, match='invalid length'): From 6c8276d7fd2d8b9779499e6577ef8c8614da6dc5 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Tue, 27 Oct 2026 18:05:47 -0700 Subject: [PATCH 23/35] Improved tests for from_text methods [skip ci] --- tests/test_bit.py | 3 +++ tests/test_half_vector.py | 13 +++++++++++++ tests/test_sparse_vector.py | 3 +++ tests/test_vector.py | 13 +++++++++++++ 4 files changed, 32 insertions(+) diff --git a/tests/test_bit.py b/tests/test_bit.py index a7df304..5f60f77 100644 --- a/tests/test_bit.py +++ b/tests/test_bit.py @@ -103,6 +103,9 @@ def test_from_text(self) -> None: assert np.array_equal(vec.to_numpy(), [True, False, True]) assert vec.to_text() == '101' + def test_from_text_zero_length(self) -> None: + assert Bit.from_text('') == Bit([]) + def test_from_binary(self) -> None: data = struct.pack('>iB', 3, 5 << 5) vec = Bit.from_binary(data) diff --git a/tests/test_half_vector.py b/tests/test_half_vector.py index 032b923..74adf97 100644 --- a/tests/test_half_vector.py +++ b/tests/test_half_vector.py @@ -67,6 +67,19 @@ def test_from_text(self) -> None: assert np.array_equal(vec.to_numpy(), [1.5, 2, 3]) assert vec.to_text() == '[1.5,2.0,3.0]' + def test_from_text_zero_dimensions(self) -> None: + # TODO no error + with pytest.raises(ValueError, match='could not convert string to float'): + HalfVector.from_text('[]') + + def test_from_text_open_bracket(self) -> None: + # TODO raise ValueError in 0.6.0 + assert HalfVector.from_text('{1]') + + def test_from_text_close_bracket(self) -> None: + # TODO raise ValueError in 0.6.0 + assert HalfVector.from_text('[1}') + def test_from_binary(self) -> None: data = struct.pack('>HH3e', 3, 0, 1.5, 2, 3) vec = HalfVector.from_binary(data) diff --git a/tests/test_sparse_vector.py b/tests/test_sparse_vector.py index e1be6b7..ae0a8d0 100644 --- a/tests/test_sparse_vector.py +++ b/tests/test_sparse_vector.py @@ -169,6 +169,9 @@ def test_from_text(self) -> None: assert np.array_equal(vec.to_numpy(), [1.5, 0, 2, 0, 3, 0]) assert vec.to_text() == '{1:1.5,3:2.0,5:3.0}/6' + def test_from_text_zero_dimensions(self) -> None: + assert SparseVector.from_text('{}/0') == SparseVector([]) + # TODO uncomment in 0.6.0 # def test_from_text_unsorted(self) -> None: # with pytest.raises(ValueError, match='sparsevec indices must be in ascending order'): diff --git a/tests/test_vector.py b/tests/test_vector.py index 544b66a..be08bc3 100644 --- a/tests/test_vector.py +++ b/tests/test_vector.py @@ -67,6 +67,19 @@ def test_from_text(self) -> None: assert np.array_equal(vec.to_numpy(), [1.5, 2, 3]) assert vec.to_text() == '[1.5,2.0,3.0]' + def test_from_text_zero_dimensions(self) -> None: + # TODO no error + with pytest.raises(ValueError, match='could not convert string to float'): + Vector.from_text('[]') + + def test_from_text_open_bracket(self) -> None: + # TODO raise ValueError in 0.6.0 + assert Vector.from_text('{1]') + + def test_from_text_close_bracket(self) -> None: + # TODO raise ValueError in 0.6.0 + assert Vector.from_text('[1}') + def test_from_binary(self) -> None: data = struct.pack('>hh3f', 3, 0, 1.5, 2, 3) vec = Vector.from_binary(data) From 90ac9314fcdc0b55fa9986e141da0bf4fddf4a5b Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Tue, 27 Oct 2026 18:07:50 -0700 Subject: [PATCH 24/35] Improved tests for from_text methods [skip ci] --- tests/test_bit.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_bit.py b/tests/test_bit.py index 5f60f77..d0ab71a 100644 --- a/tests/test_bit.py +++ b/tests/test_bit.py @@ -106,6 +106,10 @@ def test_from_text(self) -> None: def test_from_text_zero_length(self) -> None: assert Bit.from_text('') == Bit([]) + def test_from_text_invalid_character(self) -> None: + with pytest.raises(ValueError, match='expected bit string'): + Bit.from_text('2') + def test_from_binary(self) -> None: data = struct.pack('>iB', 3, 5 << 5) vec = Bit.from_binary(data) From ba94991860251f8604bb16563f920642d74c40c7 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Tue, 27 Oct 2026 18:13:19 -0700 Subject: [PATCH 25/35] Added support for empty vectors to from_text methods [skip ci] --- pgvector/halfvec.py | 2 ++ pgvector/vector.py | 2 ++ tests/test_half_vector.py | 4 +--- tests/test_vector.py | 4 +--- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pgvector/halfvec.py b/pgvector/halfvec.py index 8658b09..2b3b325 100644 --- a/pgvector/halfvec.py +++ b/pgvector/halfvec.py @@ -88,6 +88,8 @@ def from_binary(cls, value: bytes | bytearray | memoryview, /) -> HalfVector: @classmethod def _from_text(cls, value: str, /) -> list[float]: + if len(value) == 2: + return [] return [float(v) for v in value[1:-1].split(',')] @classmethod diff --git a/pgvector/vector.py b/pgvector/vector.py index 10f70fa..d86cba0 100644 --- a/pgvector/vector.py +++ b/pgvector/vector.py @@ -86,6 +86,8 @@ def from_binary(cls, value: bytes | bytearray | memoryview, /) -> Vector: @classmethod def _from_text(cls, value: str, /) -> list[float]: + if len(value) == 2: + return [] return [float(v) for v in value[1:-1].split(',')] @classmethod diff --git a/tests/test_half_vector.py b/tests/test_half_vector.py index 74adf97..be5251b 100644 --- a/tests/test_half_vector.py +++ b/tests/test_half_vector.py @@ -68,9 +68,7 @@ def test_from_text(self) -> None: assert vec.to_text() == '[1.5,2.0,3.0]' def test_from_text_zero_dimensions(self) -> None: - # TODO no error - with pytest.raises(ValueError, match='could not convert string to float'): - HalfVector.from_text('[]') + assert HalfVector.from_text('[]') == HalfVector([]) def test_from_text_open_bracket(self) -> None: # TODO raise ValueError in 0.6.0 diff --git a/tests/test_vector.py b/tests/test_vector.py index be08bc3..80e6502 100644 --- a/tests/test_vector.py +++ b/tests/test_vector.py @@ -68,9 +68,7 @@ def test_from_text(self) -> None: assert vec.to_text() == '[1.5,2.0,3.0]' def test_from_text_zero_dimensions(self) -> None: - # TODO no error - with pytest.raises(ValueError, match='could not convert string to float'): - Vector.from_text('[]') + assert Vector.from_text('[]') == Vector([]) def test_from_text_open_bracket(self) -> None: # TODO raise ValueError in 0.6.0 From 2a35b9b2991e6a01b706ae926a3c4351e44dca12 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Tue, 27 Oct 2026 18:16:02 -0700 Subject: [PATCH 26/35] Improved tests for from_text methods --- tests/test_half_vector.py | 4 ++++ tests/test_sparse_vector.py | 4 ++++ tests/test_vector.py | 4 ++++ 3 files changed, 12 insertions(+) diff --git a/tests/test_half_vector.py b/tests/test_half_vector.py index be5251b..688bf0f 100644 --- a/tests/test_half_vector.py +++ b/tests/test_half_vector.py @@ -67,6 +67,10 @@ def test_from_text(self) -> None: assert np.array_equal(vec.to_numpy(), [1.5, 2, 3]) assert vec.to_text() == '[1.5,2.0,3.0]' + def test_from_text_zero_length(self) -> None: + with pytest.raises(ValueError, match='could not convert string to float'): + HalfVector.from_text('') + def test_from_text_zero_dimensions(self) -> None: assert HalfVector.from_text('[]') == HalfVector([]) diff --git a/tests/test_sparse_vector.py b/tests/test_sparse_vector.py index ae0a8d0..73a68f5 100644 --- a/tests/test_sparse_vector.py +++ b/tests/test_sparse_vector.py @@ -169,6 +169,10 @@ def test_from_text(self) -> None: assert np.array_equal(vec.to_numpy(), [1.5, 0, 2, 0, 3, 0]) assert vec.to_text() == '{1:1.5,3:2.0,5:3.0}/6' + def test_from_text_zero_length(self) -> None: + with pytest.raises(ValueError, match='not enough values to unpack'): + SparseVector.from_text('') + def test_from_text_zero_dimensions(self) -> None: assert SparseVector.from_text('{}/0') == SparseVector([]) diff --git a/tests/test_vector.py b/tests/test_vector.py index 80e6502..681c925 100644 --- a/tests/test_vector.py +++ b/tests/test_vector.py @@ -67,6 +67,10 @@ def test_from_text(self) -> None: assert np.array_equal(vec.to_numpy(), [1.5, 2, 3]) assert vec.to_text() == '[1.5,2.0,3.0]' + def test_from_text_zero_length(self) -> None: + with pytest.raises(ValueError, match='could not convert string to float'): + Vector.from_text('') + def test_from_text_zero_dimensions(self) -> None: assert Vector.from_text('[]') == Vector([]) From 67913de4887f43deb1fbf3d187cb6e698861da20 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Tue, 27 Oct 2026 18:21:11 -0700 Subject: [PATCH 27/35] Simplified tests [skip ci] --- tests/test_half_vector.py | 13 ++----------- tests/test_sparse_vector.py | 18 ++---------------- tests/test_vector.py | 13 ++----------- 3 files changed, 6 insertions(+), 38 deletions(-) diff --git a/tests/test_half_vector.py b/tests/test_half_vector.py index 688bf0f..954594b 100644 --- a/tests/test_half_vector.py +++ b/tests/test_half_vector.py @@ -61,11 +61,7 @@ def test_dimensions(self) -> None: assert HalfVector([1, 2, 3]).dimensions() == 3 def test_from_text(self) -> None: - vec = HalfVector.from_text('[1.5,2,3]') - assert vec.to_list() == [1.5, 2, 3] - if np is not None: - assert np.array_equal(vec.to_numpy(), [1.5, 2, 3]) - assert vec.to_text() == '[1.5,2.0,3.0]' + assert HalfVector.from_text('[1.5,2,3]') == HalfVector([1.5, 2, 3]) def test_from_text_zero_length(self) -> None: with pytest.raises(ValueError, match='could not convert string to float'): @@ -83,12 +79,7 @@ def test_from_text_close_bracket(self) -> None: assert HalfVector.from_text('[1}') def test_from_binary(self) -> None: - data = struct.pack('>HH3e', 3, 0, 1.5, 2, 3) - vec = HalfVector.from_binary(data) - assert vec.to_list() == [1.5, 2, 3] - if np is not None: - assert np.array_equal(vec.to_numpy(), [1.5, 2, 3]) - assert vec.to_binary() == data + assert HalfVector.from_binary(struct.pack('>hh3e', 3, 0, 1.5, 2, 3)) == HalfVector([1.5, 2, 3]) def test_from_binary_zero_length(self) -> None: # TODO change to ValueError('invalid length') in 0.6.0 diff --git a/tests/test_sparse_vector.py b/tests/test_sparse_vector.py index 73a68f5..86ae333 100644 --- a/tests/test_sparse_vector.py +++ b/tests/test_sparse_vector.py @@ -160,14 +160,7 @@ def test_zero_vector_text(self) -> None: assert vec.to_list() == SparseVector.from_text(vec.to_text()).to_list() def test_from_text(self) -> None: - vec = SparseVector.from_text('{1:1.5,3:2,5:3}/6') - assert vec.dimensions() == 6 - assert vec.indices() == [0, 2, 4] - assert vec.values() == [1.5, 2, 3] - assert vec.to_list() == [1.5, 0, 2, 0, 3, 0] - if np is not None: - assert np.array_equal(vec.to_numpy(), [1.5, 0, 2, 0, 3, 0]) - assert vec.to_text() == '{1:1.5,3:2.0,5:3.0}/6' + assert SparseVector.from_text('{1:1.5,3:2,5:3}/6') == SparseVector([1.5, 0, 2, 0, 3, 0]) def test_from_text_zero_length(self) -> None: with pytest.raises(ValueError, match='not enough values to unpack'): @@ -188,14 +181,7 @@ def test_from_text_zero_dimensions(self) -> None: def test_from_binary(self) -> None: data = struct.pack('>iii3i3f', 6, 3, 0, 0, 2, 4, 1.5, 2, 3) - vec = SparseVector.from_binary(data) - assert vec.dimensions() == 6 - assert vec.indices() == [0, 2, 4] - assert vec.values() == [1.5, 2, 3] - assert vec.to_list() == [1.5, 0, 2, 0, 3, 0] - if np is not None: - assert np.array_equal(vec.to_numpy(), [1.5, 0, 2, 0, 3, 0]) - assert vec.to_binary() == data + assert SparseVector.from_binary(data) == SparseVector([1.5, 0, 2, 0, 3, 0]) def test_from_binary_zero_length(self) -> None: # TODO change to ValueError('invalid length') in 0.6.0 diff --git a/tests/test_vector.py b/tests/test_vector.py index 681c925..d4b1138 100644 --- a/tests/test_vector.py +++ b/tests/test_vector.py @@ -61,11 +61,7 @@ def test_dimensions(self) -> None: assert Vector([1, 2, 3]).dimensions() == 3 def test_from_text(self) -> None: - vec = Vector.from_text('[1.5,2,3]') - assert vec.to_list() == [1.5, 2, 3] - if np is not None: - assert np.array_equal(vec.to_numpy(), [1.5, 2, 3]) - assert vec.to_text() == '[1.5,2.0,3.0]' + assert Vector.from_text('[1.5,2,3]') == Vector([1.5, 2, 3]) def test_from_text_zero_length(self) -> None: with pytest.raises(ValueError, match='could not convert string to float'): @@ -83,12 +79,7 @@ def test_from_text_close_bracket(self) -> None: assert Vector.from_text('[1}') def test_from_binary(self) -> None: - data = struct.pack('>hh3f', 3, 0, 1.5, 2, 3) - vec = Vector.from_binary(data) - assert vec.to_list() == [1.5, 2, 3] - if np is not None: - assert np.array_equal(vec.to_numpy(), [1.5, 2, 3]) - assert vec.to_binary() == data + assert Vector.from_binary(struct.pack('>hh3f', 3, 0, 1.5, 2, 3)) == Vector([1.5, 2, 3]) def test_from_binary_zero_length(self) -> None: # TODO change to ValueError('invalid length') in 0.6.0 From a594893a604aebc433c000f421b9183ad39529e7 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Tue, 27 Oct 2026 18:33:25 -0700 Subject: [PATCH 28/35] Improved tests for from_text methods [skip ci] --- tests/test_half_vector.py | 4 ++++ tests/test_sparse_vector.py | 8 ++++++++ tests/test_vector.py | 4 ++++ 3 files changed, 16 insertions(+) diff --git a/tests/test_half_vector.py b/tests/test_half_vector.py index 954594b..6f3b5af 100644 --- a/tests/test_half_vector.py +++ b/tests/test_half_vector.py @@ -78,6 +78,10 @@ def test_from_text_close_bracket(self) -> None: # TODO raise ValueError in 0.6.0 assert HalfVector.from_text('[1}') + def test_from_text_non_number(self) -> None: + with pytest.raises(ValueError, match='could not convert string to float'): + assert HalfVector.from_text('[a]') + def test_from_binary(self) -> None: assert HalfVector.from_binary(struct.pack('>hh3e', 3, 0, 1.5, 2, 3)) == HalfVector([1.5, 2, 3]) diff --git a/tests/test_sparse_vector.py b/tests/test_sparse_vector.py index 86ae333..7139595 100644 --- a/tests/test_sparse_vector.py +++ b/tests/test_sparse_vector.py @@ -169,6 +169,14 @@ def test_from_text_zero_length(self) -> None: def test_from_text_zero_dimensions(self) -> None: assert SparseVector.from_text('{}/0') == SparseVector([]) + def test_from_text_non_number_index(self) -> None: + with pytest.raises(ValueError, match='invalid literal for int'): + assert SparseVector.from_text('{a:1}/1') + + def test_from_text_non_number_value(self) -> None: + with pytest.raises(ValueError, match='could not convert string to float'): + assert SparseVector.from_text('{1:a}/1') + # TODO uncomment in 0.6.0 # def test_from_text_unsorted(self) -> None: # with pytest.raises(ValueError, match='sparsevec indices must be in ascending order'): diff --git a/tests/test_vector.py b/tests/test_vector.py index d4b1138..075e862 100644 --- a/tests/test_vector.py +++ b/tests/test_vector.py @@ -78,6 +78,10 @@ def test_from_text_close_bracket(self) -> None: # TODO raise ValueError in 0.6.0 assert Vector.from_text('[1}') + def test_from_text_non_number(self) -> None: + with pytest.raises(ValueError, match='could not convert string to float'): + assert Vector.from_text('[a]') + def test_from_binary(self) -> None: assert Vector.from_binary(struct.pack('>hh3f', 3, 0, 1.5, 2, 3)) == Vector([1.5, 2, 3]) From 2109b244e6c96e8d173f97d4186f2147aa28cc4d Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Tue, 27 Oct 2026 18:35:44 -0700 Subject: [PATCH 29/35] Simplified tests [skip ci] --- tests/test_bit.py | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/tests/test_bit.py b/tests/test_bit.py index d0ab71a..4e018d0 100644 --- a/tests/test_bit.py +++ b/tests/test_bit.py @@ -97,11 +97,7 @@ def test_equality(self) -> None: assert Bit([True, False, True]) != 1 def test_from_text(self) -> None: - vec = Bit.from_text('101') - assert vec.to_list() == [True, False, True] - if np is not None: - assert np.array_equal(vec.to_numpy(), [True, False, True]) - assert vec.to_text() == '101' + assert Bit.from_text('101') == Bit('101') def test_from_text_zero_length(self) -> None: assert Bit.from_text('') == Bit([]) @@ -111,12 +107,7 @@ def test_from_text_invalid_character(self) -> None: Bit.from_text('2') def test_from_binary(self) -> None: - data = struct.pack('>iB', 3, 5 << 5) - vec = Bit.from_binary(data) - assert vec.to_list() == [True, False, True] - if np is not None: - assert np.array_equal(vec.to_numpy(), [True, False, True]) - assert vec.to_binary() == data + assert Bit.from_binary(struct.pack('>iB', 3, 5 << 5)) == Bit('101') def test_from_binary_zero_length(self) -> None: # TODO change to ValueError('invalid length') in 0.6.0 From bb914602af73d564b438a8a3d5e00d1bd63e3e62 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Tue, 27 Oct 2026 18:43:21 -0700 Subject: [PATCH 30/35] Improved test names [skip ci] --- tests/test_bit.py | 34 +++++++++++++++++----------------- tests/test_half_vector.py | 14 +++++++------- tests/test_sparse_vector.py | 32 ++++++++++++++++---------------- tests/test_vector.py | 14 +++++++------- 4 files changed, 47 insertions(+), 47 deletions(-) diff --git a/tests/test_bit.py b/tests/test_bit.py index 4e018d0..76c9d57 100644 --- a/tests/test_bit.py +++ b/tests/test_bit.py @@ -6,46 +6,46 @@ class TestBit: - def test_list(self) -> None: + def test_init_list(self) -> None: assert Bit([True, False, True]).to_list() == [True, False, True] - def test_list_empty(self) -> None: + def test_init_list_empty(self) -> None: assert Bit([]).to_list() == [] - def test_list_none(self) -> None: + def test_init_list_none(self) -> None: with pytest.raises(ValueError) as error: Bit([True, None, True]) # type: ignore assert str(error.value) == 'expected list[bool]' - def test_list_int(self) -> None: + def test_init_list_int(self) -> None: with pytest.raises(ValueError) as error: Bit([254, 7, 0]) # type: ignore assert str(error.value) == 'expected list[bool]' - def test_list_list(self) -> None: + def test_init_list_list(self) -> None: with pytest.raises(ValueError) as error: Bit([[True, False], [True, False]]) # type: ignore assert str(error.value) == 'expected list[bool]' - def test_str(self) -> None: + def test_init_str(self) -> None: assert Bit('101').to_list() == [True, False, True] - def test_str_empty(self) -> None: + def test_init_str_empty(self) -> None: assert Bit('').to_list() == [] - def test_str_two(self) -> None: + def test_init_str_two(self) -> None: with pytest.raises(ValueError) as error: Bit('201') assert str(error.value) == 'expected bit string' - def test_bytes(self) -> None: + def test_init_bytes(self) -> None: assert Bit(b'\xff\x00\xf0').to_text() == '111111110000000011110000' assert Bit(b'\xfe\x07\x00').to_text() == '111111100000011100000000' - def test_bytes_empty(self) -> None: + def test_init_bytes_empty(self) -> None: assert Bit(b'').to_text() == '' - def test_ndarray(self) -> None: + def test_init_ndarray(self) -> None: if np is None: pytest.skip('NumPy required') @@ -53,14 +53,14 @@ def test_ndarray(self) -> None: assert Bit(arr).to_list() == [True, False, True] assert np.array_equal(Bit(arr).to_numpy(), arr) - def test_ndarray_unpackbits(self) -> None: + def test_init_ndarray_unpackbits(self) -> None: if np is None: pytest.skip('NumPy required') arr = np.unpackbits(np.array([254, 7, 0], dtype=np.uint8)) assert Bit(arr).to_text() == '111111100000011100000000' - def test_ndarray_uint8(self) -> None: + def test_init_ndarray_uint8(self) -> None: if np is None: pytest.skip('NumPy required') @@ -69,7 +69,7 @@ def test_ndarray_uint8(self) -> None: Bit(arr) assert str(error.value) == 'expected elements to be boolean' - def test_ndarray_uint16(self) -> None: + def test_init_ndarray_uint16(self) -> None: if np is None: pytest.skip('NumPy required') @@ -78,12 +78,12 @@ def test_ndarray_uint16(self) -> None: Bit(arr) # type: ignore assert str(error.value) == 'expected elements to be boolean' - def test_bool(self) -> None: + def test_init_bool(self) -> None: with pytest.raises(ValueError) as error: Bit(True) # type: ignore assert str(error.value) == 'expected bytes, str, list, or ndarray' - def test_random(self) -> None: + def test_init_random(self) -> None: value = ''.join(random.choices(['0', '1'], k=random.randint(1024, 2048))) assert Bit(value).to_text() == value @@ -91,7 +91,7 @@ def test_repr(self) -> None: assert repr(Bit([True, False, True])) == 'Bit(101)' assert str(Bit([True, False, True])) == 'Bit(101)' - def test_equality(self) -> None: + def test_eq(self) -> None: assert Bit([True, False, True]) == Bit([True, False, True]) assert Bit([True, False, True]) != Bit([True, False, False]) assert Bit([True, False, True]) != 1 diff --git a/tests/test_half_vector.py b/tests/test_half_vector.py index 6f3b5af..39354ee 100644 --- a/tests/test_half_vector.py +++ b/tests/test_half_vector.py @@ -5,25 +5,25 @@ class TestHalfVector: - def test_list(self) -> None: + def test_init_list(self) -> None: arr = [1.0, 2.0, 3.0] assert HalfVector(arr).to_list() == arr assert HalfVector(arr).to_list() is not arr - def test_list_empty(self) -> None: + def test_init_list_empty(self) -> None: assert HalfVector([]).to_list() == [] - def test_list_str(self) -> None: + def test_init_list_str(self) -> None: with pytest.raises(ValueError) as error: HalfVector([1, 'two', 3]) # type: ignore assert str(error.value) == 'expected list[float]' - def test_list_list(self) -> None: + def test_init_list_list(self) -> None: with pytest.raises(ValueError) as error: HalfVector([[1, 2], [3, 4]]) # type: ignore assert str(error.value) == 'expected list[float]' - def test_ndarray(self) -> None: + def test_init_ndarray(self) -> None: if np is None: pytest.skip('NumPy required') @@ -42,7 +42,7 @@ def test_ndarray(self) -> None: HalfVector(np.array(['one', 'two', 'three'])) assert 'could not convert string to float' in str(error.value) - def test_int(self) -> None: + def test_init_int(self) -> None: with pytest.raises(ValueError) as error: HalfVector(1) # type: ignore assert str(error.value) == 'expected list or ndarray' @@ -51,7 +51,7 @@ def test_repr(self) -> None: assert repr(HalfVector([1, 2, 3])) == 'HalfVector([1.0, 2.0, 3.0])' assert str(HalfVector([1, 2, 3])) == 'HalfVector([1.0, 2.0, 3.0])' - def test_equality(self) -> None: + def test_eq(self) -> None: assert HalfVector([1, 2, 3]) == HalfVector([1, 2, 3]) assert HalfVector([1, 2, 3]) != HalfVector([1, 2, 4]) assert HalfVector([1, 2, 3]) != 1 diff --git a/tests/test_sparse_vector.py b/tests/test_sparse_vector.py index 7139595..a644260 100644 --- a/tests/test_sparse_vector.py +++ b/tests/test_sparse_vector.py @@ -10,26 +10,26 @@ class TestSparseVector: - def test_list(self) -> None: + def test_init_list(self) -> None: vec = SparseVector([1, 0, 2, 0, 3, 0]) assert vec.to_list() == [1, 0, 2, 0, 3, 0] if np is not None: assert np.array_equal(vec.to_numpy(), [1, 0, 2, 0, 3, 0]) assert vec.indices() == [0, 2, 4] - def test_list_empty(self) -> None: + def test_init_list_empty(self) -> None: assert SparseVector([]).to_list() == [] - def test_list_str(self) -> None: + def test_init_list_str(self) -> None: with pytest.raises(ValueError, match='could not convert string to float'): SparseVector([1, 'two', 3]) # type: ignore - def test_list_dimensions(self) -> None: + def test_init_list_dimensions(self) -> None: with pytest.raises(ValueError) as error: SparseVector([1, 0, 2, 0, 3, 0], 6) # type: ignore assert str(error.value) == 'extra argument' - def test_ndarray(self) -> None: + def test_init_ndarray(self) -> None: if np is None: pytest.skip('NumPy required') @@ -37,12 +37,12 @@ def test_ndarray(self) -> None: assert vec.to_list() == [1, 0, 2, 0, 3, 0] assert vec.indices() == [0, 2, 4] - def test_dict(self) -> None: + def test_init_dict(self) -> None: vec = SparseVector({2: 2, 4: 3, 0: 1, 3: 0}, 6) assert vec.to_list() == [1, 0, 2, 0, 3, 0] assert vec.indices() == [0, 2, 4] - def test_dict_empty(self) -> None: + def test_init_dict_empty(self) -> None: assert SparseVector({}, 0).to_list() == [] # TODO uncomment in 0.6.0 @@ -60,12 +60,12 @@ def test_dict_empty(self) -> None: # with pytest.raises(ValueError, match='sparsevec cannot have negative dimensions'): # SparseVector({}, -1) - def test_dict_no_dimensions(self) -> None: + def test_init_dict_no_dimensions(self) -> None: with pytest.raises(ValueError) as error: SparseVector({0: 1, 2: 2, 4: 3}) # type: ignore assert str(error.value) == 'missing dimensions' - def test_coo_array(self) -> None: + def test_init_coo_array(self) -> None: if np is None or sparse is None: pytest.skip('NumPy and SciPy required') @@ -75,7 +75,7 @@ def test_coo_array(self) -> None: assert vec.indices() == [0, 2, 4] assert isinstance(vec.values()[0], float) - def test_coo_array_duplicates(self) -> None: + def test_init_coo_array_duplicates(self) -> None: if np is None or sparse is None: pytest.skip('NumPy and SciPy required') @@ -85,7 +85,7 @@ def test_coo_array_duplicates(self) -> None: assert vec.to_list() == [0, 3, 0] assert vec.indices() == [1] - def test_coo_array_dimensions(self) -> None: + def test_init_coo_array_dimensions(self) -> None: if np is None or sparse is None: pytest.skip('NumPy and SciPy required') @@ -93,7 +93,7 @@ def test_coo_array_dimensions(self) -> None: SparseVector(sparse.coo_array(np.array([1, 0, 2, 0, 3, 0])), 6) # type: ignore assert str(error.value) == 'extra argument' - def test_coo_matrix(self) -> None: + def test_init_coo_matrix(self) -> None: if np is None or sparse is None: pytest.skip('NumPy and SciPy required') @@ -102,7 +102,7 @@ def test_coo_matrix(self) -> None: assert vec.to_list() == [1, 0, 2, 0, 3, 0] assert vec.indices() == [0, 2, 4] - def test_dok_array(self) -> None: + def test_init_dok_array(self) -> None: if np is None or sparse is None: pytest.skip('NumPy and SciPy required') @@ -111,7 +111,7 @@ def test_dok_array(self) -> None: assert vec.to_list() == [1, 0, 2, 0, 3, 0] assert vec.indices() == [0, 2, 4] - def test_csr_array(self) -> None: + def test_init_csr_array(self) -> None: if np is None or sparse is None: pytest.skip('NumPy and SciPy required') @@ -120,7 +120,7 @@ def test_csr_array(self) -> None: assert vec.to_list() == [1, 0, 2, 0, 3, 0] assert vec.indices() == [0, 2, 4] - def test_csr_matrix(self) -> None: + def test_init_csr_matrix(self) -> None: if np is None or sparse is None: pytest.skip('NumPy and SciPy required') @@ -133,7 +133,7 @@ def test_repr(self) -> None: assert repr(SparseVector([1, 0, 2, 0, 3, 0])) == 'SparseVector({0: 1.0, 2: 2.0, 4: 3.0}, 6)' assert str(SparseVector([1, 0, 2, 0, 3, 0])) == 'SparseVector({0: 1.0, 2: 2.0, 4: 3.0}, 6)' - def test_equality(self) -> None: + def test_eq(self) -> None: assert SparseVector([1, 0, 2, 0, 3, 0]) == SparseVector([1, 0, 2, 0, 3, 0]) assert SparseVector([1, 0, 2, 0, 3, 0]) != SparseVector([1, 0, 2, 0, 3, 1]) assert SparseVector([1, 0, 2, 0, 3, 0]) == SparseVector({2: 2, 4: 3, 0: 1, 3: 0}, 6) diff --git a/tests/test_vector.py b/tests/test_vector.py index 075e862..d1fc6e5 100644 --- a/tests/test_vector.py +++ b/tests/test_vector.py @@ -5,25 +5,25 @@ class TestVector: - def test_list(self) -> None: + def test_init_list(self) -> None: arr = [1.0, 2.0, 3.0] assert Vector(arr).to_list() == arr assert Vector(arr).to_list() is not arr - def test_list_empty(self) -> None: + def test_init_list_empty(self) -> None: assert Vector([]).to_list() == [] - def test_list_str(self) -> None: + def test_init_list_str(self) -> None: with pytest.raises(ValueError) as error: Vector([1, 'two', 3]) # type: ignore assert str(error.value) == 'expected list[float]' - def test_list_list(self) -> None: + def test_init_list_list(self) -> None: with pytest.raises(ValueError) as error: Vector([[1, 2], [3, 4]]) # type: ignore assert str(error.value) == 'expected list[float]' - def test_ndarray(self) -> None: + def test_init_ndarray(self) -> None: if np is None: pytest.skip('NumPy required') @@ -42,7 +42,7 @@ def test_ndarray(self) -> None: Vector(np.array(['one', 'two', 'three'])) assert 'could not convert string to float' in str(error.value) - def test_int(self) -> None: + def test_init_int(self) -> None: with pytest.raises(ValueError) as error: Vector(1) # type: ignore assert str(error.value) == 'expected list or ndarray' @@ -51,7 +51,7 @@ def test_repr(self) -> None: assert repr(Vector([1, 2, 3])) == 'Vector([1.0, 2.0, 3.0])' assert str(Vector([1, 2, 3])) == 'Vector([1.0, 2.0, 3.0])' - def test_equality(self) -> None: + def test_eq(self) -> None: assert Vector([1, 2, 3]) == Vector([1, 2, 3]) assert Vector([1, 2, 3]) != Vector([1, 2, 4]) assert Vector([1, 2, 3]) != 1 From 312ce70b78f2589379d016321e83b87eaddff9a5 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Tue, 27 Oct 2026 22:37:51 -0700 Subject: [PATCH 31/35] Added more tests --- tests/test_bit.py | 17 +++++++++++++++++ tests/test_half_vector.py | 17 +++++++++++++++++ tests/test_sparse_vector.py | 19 ++++++++++++++++--- tests/test_vector.py | 17 +++++++++++++++++ 4 files changed, 67 insertions(+), 3 deletions(-) diff --git a/tests/test_bit.py b/tests/test_bit.py index 76c9d57..34a48e3 100644 --- a/tests/test_bit.py +++ b/tests/test_bit.py @@ -96,6 +96,23 @@ def test_eq(self) -> None: assert Bit([True, False, True]) != Bit([True, False, False]) assert Bit([True, False, True]) != 1 + def test_to_list(self) -> None: + assert Bit('101').to_list() == [True, False, True] + + def test_to_numpy(self) -> None: + if np is None: + pytest.skip('NumPy required') + + arr = Bit('101').to_numpy() + assert np.array_equal(arr, np.array([True, False, True])) + assert arr.dtype == np.bool_ + + def test_to_text(self) -> None: + assert Bit('101').to_text() == '101' + + def test_to_binary(self) -> None: + assert Bit('101').to_binary() == struct.pack('>iB', 3, 5 << 5) + def test_from_text(self) -> None: assert Bit.from_text('101') == Bit('101') diff --git a/tests/test_half_vector.py b/tests/test_half_vector.py index 39354ee..d9d8580 100644 --- a/tests/test_half_vector.py +++ b/tests/test_half_vector.py @@ -60,6 +60,23 @@ def test_eq(self) -> None: def test_dimensions(self) -> None: assert HalfVector([1, 2, 3]).dimensions() == 3 + def test_to_list(self) -> None: + assert HalfVector([1.5, 2, 3]).to_list() == [1.5, 2, 3] + + def test_to_numpy(self) -> None: + if np is None: + pytest.skip('NumPy required') + + arr = HalfVector([1.5, 2, 3]).to_numpy() + assert np.array_equal(arr, np.array([1.5, 2, 3])) + assert arr.dtype == np.float16 + + def test_to_text(self) -> None: + assert HalfVector([1.5, 2, 3]).to_text() == '[1.5,2.0,3.0]' + + def test_to_binary(self) -> None: + assert HalfVector([1.5, 2, 3]).to_binary() == struct.pack('>hh3e', 3, 0, 1.5, 2, 3) + def test_from_text(self) -> None: assert HalfVector.from_text('[1.5,2,3]') == HalfVector([1.5, 2, 3]) diff --git a/tests/test_sparse_vector.py b/tests/test_sparse_vector.py index a644260..6666baf 100644 --- a/tests/test_sparse_vector.py +++ b/tests/test_sparse_vector.py @@ -155,9 +155,22 @@ def test_to_coo(self) -> None: assert np.array_equal(SparseVector([1, 0, 2, 0, 3, 0]).to_coo().toarray(), [[1, 0, 2, 0, 3, 0]]) - def test_zero_vector_text(self) -> None: - vec = SparseVector({}, 3) - assert vec.to_list() == SparseVector.from_text(vec.to_text()).to_list() + def test_to_list(self) -> None: + assert SparseVector({2: 2, 4: 3, 0: 1.5, 3: 0}, 6).to_list() == [1.5, 0, 2, 0, 3, 0] + + def test_to_numpy(self) -> None: + if np is None: + pytest.skip('NumPy required') + + arr = SparseVector({2: 2, 4: 3, 0: 1.5, 3: 0}, 6).to_numpy() + assert np.array_equal(arr, np.array([1.5, 0, 2, 0, 3, 0])) + assert arr.dtype == np.float32 + + def test_to_text(self) -> None: + assert SparseVector({2: 2, 4: 3, 0: 1.5, 3: 0}, 6).to_text() == '{1:1.5,3:2.0,5:3.0}/6' + + def test_to_binary(self) -> None: + assert SparseVector({2: 2, 4: 3, 0: 1.5, 3: 0}, 6).to_binary() == struct.pack('>iii3i3f', 6, 3, 0, 0, 2, 4, 1.5, 2, 3) def test_from_text(self) -> None: assert SparseVector.from_text('{1:1.5,3:2,5:3}/6') == SparseVector([1.5, 0, 2, 0, 3, 0]) diff --git a/tests/test_vector.py b/tests/test_vector.py index d1fc6e5..621d0fe 100644 --- a/tests/test_vector.py +++ b/tests/test_vector.py @@ -60,6 +60,23 @@ def test_eq(self) -> None: def test_dimensions(self) -> None: assert Vector([1, 2, 3]).dimensions() == 3 + def test_to_list(self) -> None: + assert Vector([1.5, 2, 3]).to_list() == [1.5, 2, 3] + + def test_to_numpy(self) -> None: + if np is None: + pytest.skip('NumPy required') + + arr = Vector([1.5, 2, 3]).to_numpy() + assert np.array_equal(arr, np.array([1.5, 2, 3])) + assert arr.dtype == np.float32 + + def test_to_text(self) -> None: + assert Vector([1.5, 2, 3]).to_text() == '[1.5,2.0,3.0]' + + def test_to_binary(self) -> None: + assert Vector([1.5, 2, 3]).to_binary() == struct.pack('>hh3f', 3, 0, 1.5, 2, 3) + def test_from_text(self) -> None: assert Vector.from_text('[1.5,2,3]') == Vector([1.5, 2, 3]) From d4ca080473d1cadd6e318fd9aeda864d02d3612e Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Tue, 27 Oct 2026 22:45:54 -0700 Subject: [PATCH 32/35] Split tests [skip ci] --- tests/test_half_vector.py | 18 ++++++++++++++++-- tests/test_vector.py | 18 ++++++++++++++++-- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/tests/test_half_vector.py b/tests/test_half_vector.py index d9d8580..c99c0b2 100644 --- a/tests/test_half_vector.py +++ b/tests/test_half_vector.py @@ -31,13 +31,27 @@ def test_init_ndarray(self) -> None: assert HalfVector(arr).to_list() == [1, 2, 3] assert HalfVector(arr).to_numpy() is not arr assert HalfVector(arr).to_numpy().dtype == np.float16 - # non-contiguous + + def test_init_ndarray_non_contiguous(self) -> None: + if np is None: + pytest.skip('NumPy required') + + arr = np.array([1, 2, 3], dtype=np.float16) assert HalfVector(np.flip(arr)).to_list() == [3, 2, 1] assert HalfVector(np.flip(arr)).to_binary() == HalfVector([3, 2, 1]).to_binary() - # big endian + + def test_init_ndarray_big_endian(self) -> None: + if np is None: + pytest.skip('NumPy required') + + arr = np.array([1, 2, 3], dtype=np.float16) assert HalfVector(arr.astype('>f2')).to_list() == [1, 2, 3] assert HalfVector(arr.astype('>f2')).to_binary() == HalfVector([1, 2, 3]).to_binary() + def test_init_ndarray_str(self) -> None: + if np is None: + pytest.skip('NumPy required') + with pytest.raises(ValueError) as error: HalfVector(np.array(['one', 'two', 'three'])) assert 'could not convert string to float' in str(error.value) diff --git a/tests/test_vector.py b/tests/test_vector.py index 621d0fe..b1eda33 100644 --- a/tests/test_vector.py +++ b/tests/test_vector.py @@ -31,13 +31,27 @@ def test_init_ndarray(self) -> None: assert Vector(arr).to_list() == [1, 2, 3] assert Vector(arr).to_numpy() is not arr assert Vector(arr).to_numpy().dtype == np.float32 - # non-contiguous + + def test_init_ndarray_non_contiguous(self) -> None: + if np is None: + pytest.skip('NumPy required') + + arr = np.array([1, 2, 3], dtype=np.float32) assert Vector(np.flip(arr)).to_list() == [3, 2, 1] assert Vector(np.flip(arr)).to_binary() == Vector([3, 2, 1]).to_binary() - # big endian + + def test_init_ndarray_big_endian(self) -> None: + if np is None: + pytest.skip('NumPy required') + + arr = np.array([1, 2, 3], dtype=np.float32) assert Vector(arr.astype('>f4')).to_list() == [1, 2, 3] assert Vector(arr.astype('>f4')).to_binary() == Vector([1, 2, 3]).to_binary() + def test_init_ndarray_str(self) -> None: + if np is None: + pytest.skip('NumPy required') + with pytest.raises(ValueError) as error: Vector(np.array(['one', 'two', 'three'])) assert 'could not convert string to float' in str(error.value) From c314b69f678eaa959f45958ef0547f4ec8672824 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Tue, 27 Oct 2026 23:00:51 -0700 Subject: [PATCH 33/35] Improved test names [skip ci] --- tests/test_bit.py | 4 ++-- tests/test_half_vector.py | 4 ++-- tests/test_sparse_vector.py | 4 ++-- tests/test_vector.py | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/test_bit.py b/tests/test_bit.py index 34a48e3..bffbd5b 100644 --- a/tests/test_bit.py +++ b/tests/test_bit.py @@ -131,11 +131,11 @@ def test_from_binary_zero_length(self) -> None: with pytest.raises(struct.error, match='unpack_from requires a buffer of at least 4 bytes'): Bit.from_binary(b'') - def test_from_binary_low_length(self) -> None: + def test_from_binary_extra_data(self) -> None: with pytest.raises(ValueError, match='invalid length'): Bit.from_binary(struct.pack('>iBB', 8, 0, 0)) - def test_from_binary_high_length(self) -> None: + def test_from_binary_missing_data(self) -> None: with pytest.raises(ValueError, match='invalid length'): Bit.from_binary(struct.pack('>iBB', 17, 0, 0)) diff --git a/tests/test_half_vector.py b/tests/test_half_vector.py index c99c0b2..f0648d5 100644 --- a/tests/test_half_vector.py +++ b/tests/test_half_vector.py @@ -121,11 +121,11 @@ def test_from_binary_zero_length(self) -> None: with pytest.raises(struct.error, match='unpack_from requires a buffer of at least 4 bytes'): HalfVector.from_binary(b'') - def test_from_binary_low_dimensions(self) -> None: + def test_from_binary_extra_data(self) -> None: with pytest.raises(ValueError, match='invalid length'): HalfVector.from_binary(struct.pack('>hhee', 1, 0, 1, 2)) - def test_from_binary_high_dimensions(self) -> None: + def test_from_binary_missing_data(self) -> None: with pytest.raises(ValueError, match='invalid length'): HalfVector.from_binary(struct.pack('>hhee', 3, 0, 1, 2)) diff --git a/tests/test_sparse_vector.py b/tests/test_sparse_vector.py index 6666baf..502e820 100644 --- a/tests/test_sparse_vector.py +++ b/tests/test_sparse_vector.py @@ -209,11 +209,11 @@ def test_from_binary_zero_length(self) -> None: with pytest.raises(struct.error, match='unpack_from requires a buffer of at least 12 bytes'): SparseVector.from_binary(b'') - def test_from_binary_low_nnz(self) -> None: + def test_from_binary_extra_data(self) -> None: with pytest.raises(ValueError, match='invalid length'): SparseVector.from_binary(struct.pack('>iii2i2f', 3, 1, 0, 0, 1, 1, 2)) - def test_from_binary_high_nnz(self) -> None: + def test_from_binary_missing_data(self) -> None: with pytest.raises(ValueError, match='invalid length'): SparseVector.from_binary(struct.pack('>iii2i2f', 3, 3, 0, 0, 1, 1, 2)) diff --git a/tests/test_vector.py b/tests/test_vector.py index b1eda33..883e3a6 100644 --- a/tests/test_vector.py +++ b/tests/test_vector.py @@ -121,11 +121,11 @@ def test_from_binary_zero_length(self) -> None: with pytest.raises(struct.error, match='unpack_from requires a buffer of at least 4 bytes'): Vector.from_binary(b'') - def test_from_binary_low_dimensions(self) -> None: + def test_from_binary_extra_data(self) -> None: with pytest.raises(ValueError, match='invalid length'): Vector.from_binary(struct.pack('>hhff', 1, 0, 1, 2)) - def test_from_binary_high_dimensions(self) -> None: + def test_from_binary_missing_data(self) -> None: with pytest.raises(ValueError, match='invalid length'): Vector.from_binary(struct.pack('>hhff', 3, 0, 1, 2)) From 062b477cde8f722aeffc9f58f7ad1f91751f848c Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Tue, 27 Oct 2026 23:27:30 -0700 Subject: [PATCH 34/35] Improved tests --- tests/test_bit.py | 8 ++++---- tests/test_half_vector.py | 8 ++++---- tests/test_sparse_vector.py | 8 ++++---- tests/test_vector.py | 8 ++++---- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/tests/test_bit.py b/tests/test_bit.py index bffbd5b..9179cd3 100644 --- a/tests/test_bit.py +++ b/tests/test_bit.py @@ -131,13 +131,13 @@ def test_from_binary_zero_length(self) -> None: with pytest.raises(struct.error, match='unpack_from requires a buffer of at least 4 bytes'): Bit.from_binary(b'') - def test_from_binary_extra_data(self) -> None: + def test_from_binary_missing_data(self) -> None: with pytest.raises(ValueError, match='invalid length'): - Bit.from_binary(struct.pack('>iBB', 8, 0, 0)) + Bit.from_binary(struct.pack('>iB', 9, 0)) - def test_from_binary_missing_data(self) -> None: + def test_from_binary_extra_data(self) -> None: with pytest.raises(ValueError, match='invalid length'): - Bit.from_binary(struct.pack('>iBB', 17, 0, 0)) + Bit.from_binary(struct.pack('>iBBB', 16, 0, 0, 0)) def test_from_binary_negative_length(self) -> None: with pytest.raises(ValueError, match='invalid length'): diff --git a/tests/test_half_vector.py b/tests/test_half_vector.py index f0648d5..a907c94 100644 --- a/tests/test_half_vector.py +++ b/tests/test_half_vector.py @@ -121,13 +121,13 @@ def test_from_binary_zero_length(self) -> None: with pytest.raises(struct.error, match='unpack_from requires a buffer of at least 4 bytes'): HalfVector.from_binary(b'') - def test_from_binary_extra_data(self) -> None: + def test_from_binary_missing_data(self) -> None: with pytest.raises(ValueError, match='invalid length'): - HalfVector.from_binary(struct.pack('>hhee', 1, 0, 1, 2)) + HalfVector.from_binary(struct.pack('>hhe', 2, 0, 1)) - def test_from_binary_missing_data(self) -> None: + def test_from_binary_extra_data(self) -> None: with pytest.raises(ValueError, match='invalid length'): - HalfVector.from_binary(struct.pack('>hhee', 3, 0, 1, 2)) + HalfVector.from_binary(struct.pack('>hheee', 2, 0, 1, 2, 3)) def test_from_binary_negative_dimensions(self) -> None: with pytest.raises(ValueError, match='invalid length'): diff --git a/tests/test_sparse_vector.py b/tests/test_sparse_vector.py index 502e820..e3bb8cc 100644 --- a/tests/test_sparse_vector.py +++ b/tests/test_sparse_vector.py @@ -209,13 +209,13 @@ def test_from_binary_zero_length(self) -> None: with pytest.raises(struct.error, match='unpack_from requires a buffer of at least 12 bytes'): SparseVector.from_binary(b'') - def test_from_binary_extra_data(self) -> None: + def test_from_binary_missing_data(self) -> None: with pytest.raises(ValueError, match='invalid length'): - SparseVector.from_binary(struct.pack('>iii2i2f', 3, 1, 0, 0, 1, 1, 2)) + SparseVector.from_binary(struct.pack('>iii2if', 2, 2, 0, 0, 1, 1)) - def test_from_binary_missing_data(self) -> None: + def test_from_binary_extra_data(self) -> None: with pytest.raises(ValueError, match='invalid length'): - SparseVector.from_binary(struct.pack('>iii2i2f', 3, 3, 0, 0, 1, 1, 2)) + SparseVector.from_binary(struct.pack('>iii2ifff', 2, 2, 0, 0, 1, 1, 2, 3)) def test_from_binary_negative_nnz(self) -> None: with pytest.raises(ValueError, match='invalid length'): diff --git a/tests/test_vector.py b/tests/test_vector.py index 883e3a6..13a9cc7 100644 --- a/tests/test_vector.py +++ b/tests/test_vector.py @@ -121,13 +121,13 @@ def test_from_binary_zero_length(self) -> None: with pytest.raises(struct.error, match='unpack_from requires a buffer of at least 4 bytes'): Vector.from_binary(b'') - def test_from_binary_extra_data(self) -> None: + def test_from_binary_missing_data(self) -> None: with pytest.raises(ValueError, match='invalid length'): - Vector.from_binary(struct.pack('>hhff', 1, 0, 1, 2)) + Vector.from_binary(struct.pack('>hhf', 2, 0, 1)) - def test_from_binary_missing_data(self) -> None: + def test_from_binary_extra_data(self) -> None: with pytest.raises(ValueError, match='invalid length'): - Vector.from_binary(struct.pack('>hhff', 3, 0, 1, 2)) + Vector.from_binary(struct.pack('>hhfff', 2, 0, 1, 2, 3)) def test_from_binary_negative_dimensions(self) -> None: with pytest.raises(ValueError, match='invalid length'): From d2ace87ca497517b73ffa2c88a50896f65a2b17a Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Wed, 4 Nov 2026 18:53:08 -0800 Subject: [PATCH 35/35] Updated Cohere example [skip ci] --- examples/cohere/example.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/cohere/example.py b/examples/cohere/example.py index 5cf8d69..9b32073 100644 --- a/examples/cohere/example.py +++ b/examples/cohere/example.py @@ -9,12 +9,12 @@ register_vector(conn) conn.execute('DROP TABLE IF EXISTS documents') -conn.execute('CREATE TABLE documents (id bigserial PRIMARY KEY, content text, embedding bit(1536))') +conn.execute('CREATE TABLE documents (id bigserial PRIMARY KEY, content text, embedding bit(2048))') def embed(input, input_type): co = cohere.ClientV2() - response = co.embed(texts=input, model='embed-v4.0', input_type=input_type, embedding_types=['ubinary']) + response = co.embed(texts=input, model='embed-v5.0-pro', input_type=input_type, embedding_types=['ubinary']) return [bytes(embedding) for embedding in response.embeddings.ubinary]