You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@polaris.apache.org by yz...@apache.org on 2026/07/21 05:51:15 UTC

(polaris) branch main updated: Fix Python CLI table command exit codes (#5091)

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

MonkeyCanCode pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/polaris.git


The following commit(s) were added to refs/heads/main by this push:
     new 8bf207aa30 Fix Python CLI table command exit codes (#5091)
8bf207aa30 is described below

commit 8bf207aa300b39baa1f0e68502f857c2f5af1b90
Author: Matt Faltyn <76...@users.noreply.github.com>
AuthorDate: Tue Jul 21 07:51:10 2026 +0200

    Fix Python CLI table command exit codes (#5091)
    
    Fixes #5090
---
 CHANGELOG.md                                       |  1 +
 client/python/apache_polaris/cli/command/tables.py | 46 ++++++++--------------
 client/python/apache_polaris/cli/polaris_cli.py    |  5 ++-
 client/python/tests/test_tables_command.py         | 37 +++++++++++++++++
 4 files changed, 58 insertions(+), 31 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3ddadba6df..0fcee873f3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -74,6 +74,7 @@ request adding CHANGELOG notes for breaking (!) changes and possibly other secti
 - Deprecated `ALLOW_EXTERNAL_TABLE_LOCATION`. Use `ALLOW_EXTERNAL_METADATA_FILE_LOCATION` for external metadata file locations, including catalog config `polaris.config.allow.external.metadata.file.location`.
 
 ### Fixes
+- Python CLI `tables list`, `tables get`, and `tables delete` commands now exit with status 1 when catalog API requests fail. Previously, these commands printed an error but exited successfully.
 - Default table storage locations with object-storage prefixing enabled are now percent-encoded with UTF-8 instead of the JVM default charset. Previously the persisted location of a table under a non-ASCII namespace or table name depended on the platform default charset, so the same table could resolve to a different (or lossy) location on a non-UTF-8 JVM.
 - Async task execution (table cleanup, manifest and batch file cleanup) now retries when a handler returns false on transient errors (e.g. IO or delete failures). Previously `false` was swallowed with only a warning log and the task was never retried via the existing retry mechanism.
 - `TableCleanupTaskHandler` now clamps `TABLE_METADATA_CLEANUP_BATCH_SIZE` to at least 1. Previously a non-positive realm override caused an infinite loop (0) or `IllegalArgumentException` (<0) when splitting metadata files for cleanup.
diff --git a/client/python/apache_polaris/cli/command/tables.py b/client/python/apache_polaris/cli/command/tables.py
index 33ed660086..a25b0b9569 100644
--- a/client/python/apache_polaris/cli/command/tables.py
+++ b/client/python/apache_polaris/cli/command/tables.py
@@ -77,39 +77,27 @@ class TableCommand(Command):
         ns_str = UNIT_SEPARATOR.join(namespace_list)
 
         if self.table_subcommand == Subcommands.LIST:
-            try:
-                result = catalog_api.list_tables(prefix=catalog_name, namespace=ns_str)
-                for table_identifier in result.identifiers:
-                    print(table_identifier.to_json())
-            except Exception as e:
-                handle_api_exception(
-                    f"Table Listing ({'.'.join(namespace_list)})",
-                    e,
-                )
+            result = catalog_api.list_tables(prefix=catalog_name, namespace=ns_str)
+            for table_identifier in result.identifiers:
+                print(table_identifier.to_json())
         elif self.table_subcommand == Subcommands.GET:
-            try:
-                print(
-                    catalog_api.load_table(
-                        prefix=catalog_name,
-                        namespace=ns_str,
-                        table=table_name,
-                    ).to_json()
-                )
-            except Exception as e:
-                handle_api_exception(f"Table Load ({table_name})", e)
-        elif self.table_subcommand == Subcommands.DELETE:
-            namespace_dot = ".".join(namespace_list)
-            print(f"De-registering table {namespace_dot}.{table_name}...")
-            try:
-                catalog_api.drop_table(
+            print(
+                catalog_api.load_table(
                     prefix=catalog_name,
                     namespace=ns_str,
                     table=table_name,
-                    purge_requested=False,
-                )
-                print(f"De-registering table {namespace_dot}.{table_name} completed")
-            except Exception as e:
-                handle_api_exception(f"Table De-registration ({table_name})", e)
+                ).to_json()
+            )
+        elif self.table_subcommand == Subcommands.DELETE:
+            namespace_dot = ".".join(namespace_list)
+            print(f"De-registering table {namespace_dot}.{table_name}...")
+            catalog_api.drop_table(
+                prefix=catalog_name,
+                namespace=ns_str,
+                table=table_name,
+                purge_requested=False,
+            )
+            print(f"De-registering table {namespace_dot}.{table_name} completed")
         elif self.table_subcommand == Subcommands.SUMMARIZE:
             self._generate_summary(api, catalog_api, ns_str)
 
diff --git a/client/python/apache_polaris/cli/polaris_cli.py b/client/python/apache_polaris/cli/polaris_cli.py
index 546ff0f624..00c02f8381 100644
--- a/client/python/apache_polaris/cli/polaris_cli.py
+++ b/client/python/apache_polaris/cli/polaris_cli.py
@@ -30,6 +30,7 @@ from apache_polaris.cli.command import Command
 from apache_polaris.cli.constants import Commands
 from apache_polaris.cli.exceptions import CliError, CLI_ERROR_EXIT_CODE
 from apache_polaris.cli.options.parser import Parser
+from apache_polaris.sdk.catalog.exceptions import ApiException as CatalogApiException
 from apache_polaris.sdk.management import PolarisDefaultApi
 from apache_polaris.sdk.management.exceptions import ApiException
 from apache_polaris.cli.command.profiles import ProfilesCommand
@@ -83,7 +84,7 @@ class PolarisCli:
         # Handlers from most specific to least: ApiException (OpenAPI client), CliError
         # (expected user/config failures with their own exit codes), NotImplementedError
         # (abstract Command misuse), then generic bugs.
-        except ApiException as e:
+        except (ApiException, CatalogApiException) as e:
             PolarisCli.print_api_exception(e)
             sys.exit(CLI_ERROR_EXIT_CODE)
         except CliError as e:
@@ -131,7 +132,7 @@ class PolarisCli:
         urllib3.PoolManager.urlopen = urlopen_wrapper
 
     @staticmethod
-    def print_api_exception(e: ApiException) -> None:
+    def print_api_exception(e: ApiException | CatalogApiException) -> None:
         try:
             error = json.loads(e.body)["error"]
             sys.stderr.write(
diff --git a/client/python/tests/test_tables_command.py b/client/python/tests/test_tables_command.py
index 9f734f907c..2f5110efd1 100644
--- a/client/python/tests/test_tables_command.py
+++ b/client/python/tests/test_tables_command.py
@@ -20,6 +20,9 @@
 from unittest.mock import patch, MagicMock
 from cli_test_utils import CLITestBase
 from apache_polaris.cli.constants import UNIT_SEPARATOR
+from apache_polaris.cli.exceptions import CLI_ERROR_EXIT_CODE
+from apache_polaris.cli.polaris_cli import PolarisCli
+from apache_polaris.sdk.catalog.exceptions import ApiException
 
 
 class TestTablesCommand(CLITestBase):
@@ -96,6 +99,40 @@ class TestTablesCommand(CLITestBase):
             purge_requested=False,
         )
 
+    @patch("apache_polaris.cli.polaris_cli.PolarisCli.print_api_exception")
+    @patch("apache_polaris.cli.command.tables.IcebergCatalogAPI")
+    @patch("apache_polaris.cli.polaris_cli.PolarisDefaultApi")
+    @patch("apache_polaris.cli.polaris_cli.ApiClientBuilder.get_api_client")
+    def test_table_api_errors_exit_with_runtime_failure(
+        self,
+        _mock_get_api_client: MagicMock,
+        mock_default_api: MagicMock,
+        mock_iceberg_api_class: MagicMock,
+        mock_print_api_exception: MagicMock,
+    ) -> None:
+        mock_default_api.return_value = self.build_mock_client()
+        mock_iceberg_api = mock_iceberg_api_class.return_value
+        cases = [
+            ("list_tables", ["tables", "list"]),
+            ("load_table", ["tables", "get", "my_table"]),
+            ("drop_table", ["tables", "delete", "my_table"]),
+        ]
+
+        for api_method, args in cases:
+            with self.subTest(api_method=api_method):
+                method = getattr(mock_iceberg_api, api_method)
+                method.side_effect = ApiException(status=404, reason="Not Found")
+
+                with self.assertRaises(SystemExit) as cm:
+                    PolarisCli.execute(
+                        [*args, "--catalog", "my-catalog", "--namespace", "ns1"]
+                    )
+
+                self.assertEqual(cm.exception.code, CLI_ERROR_EXIT_CODE)
+                mock_print_api_exception.assert_called_once()
+                method.side_effect = None
+                mock_print_api_exception.reset_mock()
+
     @patch("apache_polaris.cli.command.tables.PolicyAPI")
     @patch("apache_polaris.cli.command.tables.IcebergCatalogAPI")
     def test_table_summarize(