From ecd157d006383fa8561d65ba2b62937eee671663 Mon Sep 17 00:00:00 2001 From: mayuriphad Date: Sun, 4 Oct 2026 09:37:25 +0530 Subject: [PATCH] Treat missing as optional in UpdateNamespacePropertiesResponse The REST spec marks the missing field of UpdateNamespacePropertiesResponse as optional and nullable, and servers such as Lakekeeper omit it when no requested removal was missing. pyiceberg required it, so update_namespace_properties raised a ValidationError after the server had applied the change. Default it to an empty list and map null to []. Closes #4066 --- pyiceberg/catalog/rest/__init__.py | 7 ++++++- tests/catalog/test_rest.py | 19 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/pyiceberg/catalog/rest/__init__.py b/pyiceberg/catalog/rest/__init__.py index 88bbd29bef..c261dcb06f 100644 --- a/pyiceberg/catalog/rest/__init__.py +++ b/pyiceberg/catalog/rest/__init__.py @@ -447,7 +447,12 @@ def replace_none_with_dict(cls, v: Any) -> Properties: class UpdateNamespacePropertiesResponse(IcebergBaseModel): removed: list[str] = Field() updated: list[str] = Field() - missing: list[str] = Field() + # optional and nullable in the REST spec; servers may omit it when nothing is missing + missing: list[str] = Field(default_factory=list) + + @field_validator("missing", mode="before") + def missing_null_to_empty(cls, missing: list[str] | None) -> list[str]: + return missing if missing is not None else [] class ListTableResponseEntry(IcebergBaseModel): diff --git a/tests/catalog/test_rest.py b/tests/catalog/test_rest.py index b06281bcef..31801d25cb 100644 --- a/tests/catalog/test_rest.py +++ b/tests/catalog/test_rest.py @@ -1342,6 +1342,25 @@ def test_update_namespace_properties_200(rest_mock: Mocker) -> None: assert response == PropertiesUpdateSummary(removed=[], updated=["prop"], missing=["abc"]) +@pytest.mark.parametrize( + "response_json", + [ + {"removed": ["a", "b"], "updated": []}, + {"removed": ["a", "b"], "updated": [], "missing": None}, + ], +) +def test_update_namespace_properties_200_without_missing(rest_mock: Mocker, response_json: dict[str, Any]) -> None: + rest_mock.post( + f"{TEST_URI}v1/namespaces/fokko/properties", + json=response_json, + status_code=200, + request_headers=TEST_HEADERS, + ) + response = RestCatalog("rest", uri=TEST_URI, token=TEST_TOKEN).update_namespace_properties(("fokko",), {"a", "b"}) + + assert response == PropertiesUpdateSummary(removed=["a", "b"], updated=[], missing=[]) + + def test_namespace_exists_200(rest_mock: Mocker) -> None: rest_mock.head( f"{TEST_URI}v1/namespaces/fokko",