This is an automated email from the ASF dual-hosted git repository.

github-merge-queue[bot] pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/iceberg-python.git


The following commit(s) were added to refs/heads/main by this push:
     new eb30d8ae6 REST: Stop double-encoding the parent namespace in 
list_namespaces (#4112)
eb30d8ae6 is described below

commit eb30d8ae6ac4054f751b91cb04d4ae5026354cc2
Author: Dhruv <[email protected]>
AuthorDate: Sun Oct 18 12:26:59 2026 +0000

    REST: Stop double-encoding the parent namespace in list_namespaces (#4112)
    
    list_namespaces passes the parent namespace through requests' params,
    which URL-encodes query values. The namespace parts were already quoted
    with _encode_namespace_path, so any part containing a space, '/', '%'
    or non-ASCII character was encoded twice and the server received e.g.
    "my%20namespace" instead of "my namespace". Join the raw parts with the
    namespace separator instead, matching RESTUtil.namespaceToQueryParam in
    the Java client.
---
 pyiceberg/catalog/rest/__init__.py |  3 ++-
 tests/catalog/test_rest.py         | 37 +++++++++++++++++++++++++++++++++++++
 2 files changed, 39 insertions(+), 1 deletion(-)

diff --git a/pyiceberg/catalog/rest/__init__.py 
b/pyiceberg/catalog/rest/__init__.py
index 88bbd29be..2b8b4496f 100644
--- a/pyiceberg/catalog/rest/__init__.py
+++ b/pyiceberg/catalog/rest/__init__.py
@@ -1676,7 +1676,8 @@ class RestCatalog(Catalog):
 
         while True:
             if namespace_tuple:
-                params["parent"] = self._encode_namespace_path(namespace_tuple)
+                # Query parameters are URL-encoded by requests, so the parts 
must not be quoted here as well
+                params["parent"] = 
self._namespace_separator.join(namespace_tuple)
             if page_token:
                 params["pageToken"] = page_token
             response = self._session.get(self.url(Endpoints.list_namespaces), 
params=params)
diff --git a/tests/catalog/test_rest.py b/tests/catalog/test_rest.py
index b06281bce..4877ee2f7 100644
--- a/tests/catalog/test_rest.py
+++ b/tests/catalog/test_rest.py
@@ -23,6 +23,7 @@ from collections.abc import Callable, Iterator
 from contextlib import contextmanager
 from typing import Any, cast
 from unittest import mock
+from urllib.parse import parse_qs, urlsplit
 
 import pytest
 from requests import Request
@@ -972,6 +973,42 @@ def test_list_namespace_with_parent_200(rest_mock: Mocker) 
-> None:
     ]
 
 
[email protected](
+    "parent",
+    [
+        ("accounting",),
+        ("accounting", "tax"),
+        ("my namespace",),
+        ("dept/finance", "tax"),
+        ("100%",),
+        ("\u65e5\u672c",),
+    ],
+)
+def test_list_namespace_with_parent_is_encoded_once(rest_mock: Mocker, parent: 
tuple[str, ...]) -> None:
+    rest_mock.get(
+        f"{TEST_URI}v1/namespaces",
+        json={"namespaces": [[*parent, "child"]]},
+        status_code=200,
+        request_headers=TEST_HEADERS,
+    )
+    assert RestCatalog("rest", uri=TEST_URI, 
token=TEST_TOKEN).list_namespaces(parent) == [(*parent, "child")]
+    query = parse_qs(urlsplit(rest_mock.last_request.url).query)
+    assert query["parent"] == ["\x1f".join(parent)]
+
+
+def test_list_namespace_with_parent_custom_separator(rest_mock: Mocker) -> 
None:
+    rest_mock.get(
+        f"{TEST_URI}v1/namespaces",
+        json={"namespaces": [["my namespace", "tax", "child"]]},
+        status_code=200,
+        request_headers=TEST_HEADERS,
+    )
+    catalog = RestCatalog("rest", uri=TEST_URI, token=TEST_TOKEN, 
**{"namespace-separator": "."})
+    assert catalog.list_namespaces(("my namespace", "tax")) == [("my 
namespace", "tax", "child")]
+    query = parse_qs(urlsplit(rest_mock.last_request.url).query)
+    assert query["parent"] == ["my namespace.tax"]
+
+
 def test_list_namespaces_paginated_200(rest_mock: Mocker) -> None:
     # First page with next-page-token
     rest_mock.get(

Reply via email to