From c16216e1c4c4f00782f467d206ce59c976550884 Mon Sep 17 00:00:00 2001 From: Yang Cen Date: Tue, 3 Nov 2026 16:02:49 +0800 Subject: [PATCH] fix(python): include namespaces in remote table repr (#4799) A synchronous remote table opened in a namespace is displayed as if it were in the database root. For example, `db.open_table("test", namespace_path=["analytics", "features"])` shows `RemoteTable(dev.test)`. Include the cached namespace path using the existing `$`-delimited table identifier: `RemoteTable(dev.analytics$features$test)`. Root output and the leaf `table.name` stay the same. Using cached identity also keeps repr available after pickle without reopening the native handle or making a request. Addresses the repr portion of ENT-3559. The native formatter already carries the qualified identifier used by Python async and TypeScript; this is a Python sync wrapper fix with no new API or naming rules. Validation: - Before the fix, three namespaced sync cases failed while root and async controls passed. All six cases now pass through the real SDK and mock HTTP endpoint. - Covers default/explicit root, nested paths, valid dotted/mixed-case/dashed names, and repr after pickle with the server stopped. Requests remain limited to the original table open. - Rebuilt current editable bindings in the repository's uv environment with Python 3.12 and pinned Rust 1.97.0 through mbx. - Ruff check and format check pass for the Python package (178 files). - Required repository-wide Ruff check reports 48 pre-existing diagnostics in six `docs/web-tests` files; the repository-wide format check reports nine unchanged documentation files. Their source matches the base commit. No lint suppression or documentation cleanup is included. --- python/python/lancedb/remote/table.py | 3 +- python/python/tests/test_remote_db.py | 59 +++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/python/python/lancedb/remote/table.py b/python/python/lancedb/remote/table.py index b8d4d18c3..dcea1f8eb 100644 --- a/python/python/lancedb/remote/table.py +++ b/python/python/lancedb/remote/table.py @@ -209,7 +209,8 @@ class RemoteTable(Table): LOOP.run(self._table.replace_field_metadata(field_name, new_metadata)) def __repr__(self) -> str: - return f"RemoteTable({self.db_name}.{self.name})" + identifier = "$".join([*self._namespace_path, self.name]) + return f"RemoteTable({self.db_name}.{identifier})" @property def schema(self) -> pa.Schema: diff --git a/python/python/tests/test_remote_db.py b/python/python/tests/test_remote_db.py index dbf9cc4d6..a57e85100 100644 --- a/python/python/tests/test_remote_db.py +++ b/python/python/tests/test_remote_db.py @@ -147,6 +147,65 @@ def missing_table_handler(cached, namespace_path): return handler, request_ids +@pytest.mark.parametrize( + "namespace_path,name,identifier", + [ + pytest.param(None, "test", "test", id="default-root"), + pytest.param([], "test", "test", id="explicit-root"), + pytest.param(["analytics"], "test", "analytics$test", id="namespace"), + pytest.param( + ["analytics", "features"], + "test", + "analytics$features$test", + id="nested", + ), + pytest.param( + ["Team.Images", "raw-items"], + "frame.info", + "Team.Images$raw-items$frame.info", + id="sql-quoted-names", + ), + ], +) +def test_remote_table_repr_namespace(namespace_path, name, identifier): + requests = [] + path = f"/v1/table/{identifier}/describe/" + + def handler(request): + requests.append(request.path) + assert request.path == path + send_json(request, {}) + + expected = f"RemoteTable(dev.{identifier})" + with mock_lancedb_connection(handler) as db: + table = db.open_table(name, namespace_path=namespace_path) + assert table.name == name + assert repr(table) == expected + restored = pickle.loads(pickle.dumps(table)) + + # A diagnostic repr must work from cached identity without reopening the handle. + assert repr(restored) == expected + assert requests == [path] + + +@pytest.mark.asyncio +async def test_async_remote_table_repr_namespace(): + requests = [] + path = "/v1/table/analytics$features$frame.info/describe/" + + def handler(request): + requests.append(request.path) + assert request.path == path + send_json(request, {}) + + async with mock_lancedb_connection_async(handler) as db: + table = await db.open_table( + "frame.info", namespace_path=["analytics", "features"] + ) + assert repr(table) == "RemoteTable(analytics$features$frame.info)" + assert requests == [path] + + @pytest.mark.parametrize("cached", [False, True]) @pytest.mark.parametrize("namespace_path", [[], ["ns"]]) def test_remote_open_missing_table(cached, namespace_path, missing_table_handler):