From 47a999d418f2fb5307e025744ed89baa16a772ec Mon Sep 17 00:00:00 2001 From: Nishchay Mahor Date: Sun, 5 Jul 2026 00:06:58 -0700 Subject: [PATCH] fix: validate is_in/not_in receive a list in the filter builder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Field.is_in() and Field.not_in() accepted any value, so Field("genre").is_in("drama") silently produced {"genre": {"$in": "drama"}} — an invalid filter the server rejects with an opaque 400. This is inconsistent with gt/gte/lt/lte, which already validate via _require_numeric. Add a matching _require_list guard (accepts list or tuple) and call it from is_in/not_in, with unit tests mirroring the numeric ones. --- pinecone/utils/filter_builder.py | 8 ++++++++ tests/unit/utils/test_filter_builder.py | 12 ++++++++++++ 2 files changed, 20 insertions(+) diff --git a/pinecone/utils/filter_builder.py b/pinecone/utils/filter_builder.py index 28e2d80b..2f7f8c74 100644 --- a/pinecone/utils/filter_builder.py +++ b/pinecone/utils/filter_builder.py @@ -117,12 +117,20 @@ def __ne__(self, value: object) -> Condition: # type: ignore[override] # -- set operators -------------------------------------------------------- + def _require_list(self, op: str, values: Any) -> None: + if not isinstance(values, (list, tuple)): + raise TypeError( + f"{op} requires a list of values, got {type(values).__name__}" + ) + def is_in(self, values: list[str | int | float | bool]) -> Condition: """``$in`` — value is in the given list.""" + self._require_list("is_in", values) return Condition({self._name: {"$in": values}}) def not_in(self, values: list[str | int | float | bool]) -> Condition: """``$nin`` — value is not in the given list.""" + self._require_list("not_in", values) return Condition({self._name: {"$nin": values}}) # -- exists operator ------------------------------------------------------ diff --git a/tests/unit/utils/test_filter_builder.py b/tests/unit/utils/test_filter_builder.py index 4b31ffff..cd25649b 100644 --- a/tests/unit/utils/test_filter_builder.py +++ b/tests/unit/utils/test_filter_builder.py @@ -64,6 +64,18 @@ def test_field_not_in(self) -> None: result = Field("tag").not_in(["spam"]).to_dict() assert result == {"tag": {"$nin": ["spam"]}} + def test_field_is_in_accepts_tuple(self) -> None: + result = Field("color").is_in(("red", "blue")).to_dict() + assert result == {"color": {"$in": ("red", "blue")}} + + def test_is_in_requires_list(self) -> None: + with pytest.raises(TypeError, match="list"): + Field("genre").is_in("drama") # type: ignore[arg-type] + + def test_not_in_requires_list(self) -> None: + with pytest.raises(TypeError, match="list"): + Field("genre").not_in("drama") # type: ignore[arg-type] + class TestFieldExists: def test_field_exists(self) -> None: