Skip to content

Commit 88a845c

Browse files
committed
fix(backups): omit description from wire payload when not provided
## Purpose `Backups.create` was unconditionally writing `body["description"] = description` with a default of `""`. This prevented callers from ever reaching the backend's `None` storage state — every backup received `description: Some("")` instead of `None`, making the two states indistinguishable. ## Solution Changed `description` parameter from `str = ""` to `str | None = None` in both sync (`Backups.create`) and async (`AsyncBackups.create`) implementations, and in both backcompat shims (`Pinecone.create_backup`, `AsyncPinecone.create_backup`). The body assignment is now conditional on `description is not None`, so omitting the parameter leaves the field absent from the JSON payload. Updated unit tests to assert the new behaviour: description absent when not provided, present when explicitly passed (including `""`).
1 parent 1994d8a commit 88a845c

9 files changed

Lines changed: 77 additions & 23 deletions

File tree

‎pinecone/_client.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -690,7 +690,7 @@ def create_backup(
690690
*,
691691
index_name: str,
692692
backup_name: str | None = None,
693-
description: str = "",
693+
description: str | None = None,
694694
) -> BackupModel:
695695
"""Backwards-compatibility shim for :meth:`Pinecone.backups.create`.
696696

‎pinecone/async_client/backups.py‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,14 +48,15 @@ async def create(
4848
*,
4949
index_name: str,
5050
name: str | None = None,
51-
description: str = "",
51+
description: str | None = None,
5252
) -> BackupModel:
5353
"""Create a backup of an existing index.
5454
5555
Args:
5656
index_name (str): Name of the index to back up.
5757
name (str | None): Optional name for the backup.
58-
description (str): Description for the backup (defaults to empty string).
58+
description (str | None): Description for the backup. When ``None``
59+
(the default), no description is sent and the backend stores ``None``.
5960
6061
Returns:
6162
A :class:`BackupModel` describing the created backup.
@@ -90,7 +91,8 @@ async def create(
9091
body: dict[str, Any] = {}
9192
if name is not None:
9293
body["name"] = name
93-
body["description"] = description
94+
if description is not None:
95+
body["description"] = description
9496
logger.info("Creating backup for index %r", index_name)
9597
response = await self._http.post(f"/indexes/{index_name}/backups", json=body)
9698
result = self._adapter.to_backup(response.content)

‎pinecone/async_client/pinecone.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -618,7 +618,7 @@ async def create_backup(
618618
*,
619619
index_name: str,
620620
backup_name: str | None = None,
621-
description: str = "",
621+
description: str | None = None,
622622
) -> BackupModel:
623623
"""Backwards-compatibility shim for :meth:`AsyncPinecone.backups.create`.
624624

‎pinecone/client/backups.py‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,14 +47,15 @@ def create(
4747
*,
4848
index_name: str,
4949
name: str | None = None,
50-
description: str = "",
50+
description: str | None = None,
5151
) -> BackupModel:
5252
"""Create a backup of an existing index.
5353
5454
Args:
5555
index_name (str): Name of the index to back up.
5656
name (str | None): Optional name for the backup.
57-
description (str): Description for the backup (defaults to empty string).
57+
description (str | None): Description for the backup. When ``None``
58+
(the default), no description is sent and the backend stores ``None``.
5859
5960
Returns:
6061
A :class:`BackupModel` describing the created backup.
@@ -80,7 +81,8 @@ def create(
8081
body: dict[str, Any] = {}
8182
if name is not None:
8283
body["name"] = name
83-
body["description"] = description
84+
if description is not None:
85+
body["description"] = description
8486
logger.info("Creating backup for index %r", index_name)
8587
response = self._http.post(f"/indexes/{index_name}/backups", json=body)
8688
result = self._adapter.to_backup(response.content)

‎tests/unit/test_async_backups.py‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from __future__ import annotations
44

5+
import json
56
from collections.abc import AsyncGenerator
67

78
import httpx
@@ -49,10 +50,10 @@ async def test_async_create_backup(async_backups: AsyncBackups) -> None:
4950
assert isinstance(result, BackupModel)
5051
assert result.backup_id == "670e8400-e29b-41d4-a716-446655440001"
5152

52-
# Verify body contains empty description when no optional params
53+
# Verify description is absent from body when not provided
5354
request = route.calls[0].request
54-
expected_body = httpx.Request("POST", "/", json={"description": ""})
55-
assert request.content == expected_body.content
55+
body = json.loads(request.content)
56+
assert "description" not in body
5657

5758

5859
@respx.mock

‎tests/unit/test_async_pinecone_backcompat.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ async def test_async_create_backup_delegate_forwards() -> None:
223223
pc, mock_backups = _make_async_pc_with_mock_backups()
224224
await pc.create_backup(index_name="my-index", backup_name="my-backup")
225225
mock_backups.create.assert_called_once_with(
226-
index_name="my-index", name="my-backup", description=""
226+
index_name="my-index", name="my-backup", description=None
227227
)
228228

229229

‎tests/unit/test_backup_description_default.py‎

Lines changed: 54 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""Unit tests for backup description default — empty string when not provided."""
1+
"""Unit tests for backup description default — absent from wire payload when not provided."""
22

33
from __future__ import annotations
44

@@ -45,14 +45,44 @@ def async_backups(async_http_client: AsyncHTTPClient) -> AsyncBackups:
4545

4646

4747
@respx.mock
48-
def test_create_backup_sends_empty_description_by_default(backups: Backups) -> None:
49-
"""When description is not provided, an empty string is sent in the body."""
48+
def test_create_backup_description_default(backups: Backups) -> None:
49+
"""When description is not provided, it is absent from the wire payload."""
5050
route = respx.post(f"{BASE_URL}/indexes/test/backups").mock(
5151
return_value=httpx.Response(201, json=make_backup_response()),
5252
)
5353

5454
result = backups.create(index_name="test")
5555

56+
assert isinstance(result, BackupModel)
57+
request = route.calls[0].request
58+
body = json.loads(request.content)
59+
assert "description" not in body
60+
61+
62+
@respx.mock
63+
def test_create_backup_explicit_description_sent(backups: Backups) -> None:
64+
"""When description is explicitly provided, it is included in the wire payload."""
65+
route = respx.post(f"{BASE_URL}/indexes/test/backups").mock(
66+
return_value=httpx.Response(201, json=make_backup_response()),
67+
)
68+
69+
result = backups.create(index_name="test", description="my description")
70+
71+
assert isinstance(result, BackupModel)
72+
request = route.calls[0].request
73+
body = json.loads(request.content)
74+
assert body["description"] == "my description"
75+
76+
77+
@respx.mock
78+
def test_create_backup_empty_string_description_sent(backups: Backups) -> None:
79+
"""When description is explicitly set to empty string, it is sent in the payload."""
80+
route = respx.post(f"{BASE_URL}/indexes/test/backups").mock(
81+
return_value=httpx.Response(201, json=make_backup_response()),
82+
)
83+
84+
result = backups.create(index_name="test", description="")
85+
5686
assert isinstance(result, BackupModel)
5787
request = route.calls[0].request
5888
body = json.loads(request.content)
@@ -62,10 +92,10 @@ def test_create_backup_sends_empty_description_by_default(backups: Backups) -> N
6292

6393
@respx.mock
6494
@pytest.mark.anyio
65-
async def test_async_create_backup_sends_empty_description_by_default(
95+
async def test_async_create_backup_description_default(
6696
async_backups: AsyncBackups,
6797
) -> None:
68-
"""Async variant: empty string description sent when not provided."""
98+
"""Async variant: description absent from wire payload when not provided."""
6999
route = respx.post(f"{BASE_URL}/indexes/test/backups").mock(
70100
return_value=httpx.Response(201, json=make_backup_response()),
71101
)
@@ -75,5 +105,22 @@ async def test_async_create_backup_sends_empty_description_by_default(
75105
assert isinstance(result, BackupModel)
76106
request = route.calls[0].request
77107
body = json.loads(request.content)
78-
assert "description" in body
79-
assert body["description"] == ""
108+
assert "description" not in body
109+
110+
111+
@respx.mock
112+
@pytest.mark.anyio
113+
async def test_async_create_backup_explicit_description_sent(
114+
async_backups: AsyncBackups,
115+
) -> None:
116+
"""Async variant: explicit description is included in the wire payload."""
117+
route = respx.post(f"{BASE_URL}/indexes/test/backups").mock(
118+
return_value=httpx.Response(201, json=make_backup_response()),
119+
)
120+
121+
result = await async_backups.create(index_name="test", description="async desc")
122+
123+
assert isinstance(result, BackupModel)
124+
request = route.calls[0].request
125+
body = json.loads(request.content)
126+
assert body["description"] == "async desc"

‎tests/unit/test_backups.py‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
from __future__ import annotations
44

5+
import json
6+
57
import httpx
68
import pytest
79
import respx
@@ -45,10 +47,10 @@ def test_create_backup_minimal(backups: Backups) -> None:
4547
assert isinstance(result, BackupModel)
4648
assert result.backup_id == "670e8400-e29b-41d4-a716-446655440001"
4749

48-
# Verify body contains empty description when no optional params
50+
# Verify description is absent from body when not provided
4951
request = route.calls[0].request
50-
expected_body = httpx.Request("POST", "/", json={"description": ""})
51-
assert request.content == expected_body.content
52+
body = json.loads(request.content)
53+
assert "description" not in body
5254

5355

5456
@respx.mock

‎tests/unit/test_pinecone_class.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -342,7 +342,7 @@ def test_forwards(self) -> None:
342342
pc, mock_backups = _make_pc_with_mock_backups()
343343
pc.create_backup(index_name="my-index", backup_name="my-backup")
344344
mock_backups.create.assert_called_once_with(
345-
index_name="my-index", name="my-backup", description=""
345+
index_name="my-index", name="my-backup", description=None
346346
)
347347

348348

0 commit comments

Comments
 (0)