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(