diff --git a/python/src/lance_namespace_impls/glue.py b/python/src/lance_namespace_impls/glue.py index dca7dda..ca8fe43 100644 --- a/python/src/lance_namespace_impls/glue.py +++ b/python/src/lance_namespace_impls/glue.py @@ -352,6 +352,8 @@ def list_tables(self, request: ListTablesRequest) -> ListTablesResponse: def describe_table(self, request: DescribeTableRequest) -> DescribeTableResponse: """Describe a table.""" + if not isinstance(request, DescribeTableRequest): + request = DescribeTableRequest.from_dict(dict(request)) if request.load_detailed_metadata: raise RuntimeError( "load_detailed_metadata=true is not supported for this implementation" diff --git a/python/src/lance_namespace_impls/hive2.py b/python/src/lance_namespace_impls/hive2.py index 8eeac27..e1fb8a1 100644 --- a/python/src/lance_namespace_impls/hive2.py +++ b/python/src/lance_namespace_impls/hive2.py @@ -378,6 +378,8 @@ def describe_table(self, request: DescribeTableRequest) -> DescribeTableResponse Only load_detailed_metadata=false is supported. """ + if not isinstance(request, DescribeTableRequest): + request = DescribeTableRequest.from_dict(dict(request)) if request.load_detailed_metadata: raise ValueError( "load_detailed_metadata=true is not supported for this implementation" diff --git a/python/src/lance_namespace_impls/hive3.py b/python/src/lance_namespace_impls/hive3.py index 7395d71..b3d2435 100644 --- a/python/src/lance_namespace_impls/hive3.py +++ b/python/src/lance_namespace_impls/hive3.py @@ -460,6 +460,8 @@ def describe_table(self, request: DescribeTableRequest) -> DescribeTableResponse Only load_detailed_metadata=false is supported. """ + if not isinstance(request, DescribeTableRequest): + request = DescribeTableRequest.from_dict(dict(request)) if request.load_detailed_metadata: raise ValueError( "load_detailed_metadata=true is not supported for this implementation" diff --git a/python/src/lance_namespace_impls/iceberg.py b/python/src/lance_namespace_impls/iceberg.py index 08c5795..bce3ad4 100644 --- a/python/src/lance_namespace_impls/iceberg.py +++ b/python/src/lance_namespace_impls/iceberg.py @@ -483,6 +483,8 @@ def describe_table(self, request: DescribeTableRequest) -> DescribeTableResponse The first element of request.id is the warehouse. Middle elements are the namespace, last element is the table name. """ + if not isinstance(request, DescribeTableRequest): + request = DescribeTableRequest.from_dict(dict(request)) if request.load_detailed_metadata: raise InvalidInputException( "load_detailed_metadata=true is not supported for this implementation" diff --git a/python/src/lance_namespace_impls/polaris.py b/python/src/lance_namespace_impls/polaris.py index 666f4fc..2f0d7d2 100644 --- a/python/src/lance_namespace_impls/polaris.py +++ b/python/src/lance_namespace_impls/polaris.py @@ -366,6 +366,8 @@ def describe_table(self, request: DescribeTableRequest) -> DescribeTableResponse Only load_detailed_metadata=false is supported. """ + if not isinstance(request, DescribeTableRequest): + request = DescribeTableRequest.from_dict(dict(request)) if request.load_detailed_metadata: raise InvalidInputException( "load_detailed_metadata=true is not supported for this implementation" diff --git a/python/src/lance_namespace_impls/unity.py b/python/src/lance_namespace_impls/unity.py index f873a83..eeea244 100644 --- a/python/src/lance_namespace_impls/unity.py +++ b/python/src/lance_namespace_impls/unity.py @@ -503,6 +503,8 @@ def declare_table(self, request: DeclareTableRequest) -> DeclareTableResponse: def describe_table(self, request: DescribeTableRequest) -> DescribeTableResponse: """Describe a table.""" + if not isinstance(request, DescribeTableRequest): + request = DescribeTableRequest.from_dict(dict(request)) if request.load_detailed_metadata: raise InvalidInputException( "load_detailed_metadata=true is not supported for this implementation" diff --git a/python/tests/test_glue.py b/python/tests/test_glue.py index 85155de..36a755d 100644 --- a/python/tests/test_glue.py +++ b/python/tests/test_glue.py @@ -302,6 +302,29 @@ def test_describe_table(self, glue_namespace): assert response.location == "s3://bucket/table.lance" + def test_describe_table_accepts_dict_request(self, glue_namespace): + """pylance's Rust namespace bridge passes the request as a plain dict + (DictWithModelDump), not a DescribeTableRequest model. describe_table must + coerce it instead of raising AttributeError on attribute access. See #181.""" + glue_namespace.glue.get_table.return_value = { + "Table": { + "Name": "test_table", + "Parameters": {"table_type": "LANCE"}, + "StorageDescriptor": {"Location": "s3://bucket/table.lance"}, + } + } + + class DictWithModelDump(dict): + def model_dump(self): + return dict(self) + + request = DictWithModelDump( + {"id": ["test_db", "test_table"], "load_detailed_metadata": False} + ) + response = glue_namespace.describe_table(request) + + assert response.location == "s3://bucket/table.lance" + def test_describe_table_not_lance(self, glue_namespace): """Test describing a non-Lance table.""" glue_namespace.glue.get_table.return_value = { diff --git a/python/tests/test_hive2.py b/python/tests/test_hive2.py index 01c9cae..7e6ba0f 100644 --- a/python/tests/test_hive2.py +++ b/python/tests/test_hive2.py @@ -219,6 +219,32 @@ def test_describe_table(self, hive_namespace, mock_hive_client): mock_client_instance.get_table.assert_called_once_with("test_db", "test_table") + def test_describe_table_accepts_dict_request( + self, hive_namespace, mock_hive_client + ): + """pylance's Rust namespace bridge passes the request as a plain dict + (DictWithModelDump), not a DescribeTableRequest model. describe_table must + coerce it instead of raising AttributeError on attribute access. See #181.""" + mock_table = MagicMock() + mock_table.sd.location = "/tmp/warehouse/test_db.db/test_table" + mock_table.owner = "table_owner" + mock_table.parameters = {"table_type": "lance", "version": "42"} + + mock_client_instance = MagicMock() + mock_client_instance.get_table.return_value = mock_table + mock_hive_client.__enter__.return_value = mock_client_instance + + class DictWithModelDump(dict): + def model_dump(self): + return dict(self) + + request = DictWithModelDump( + {"id": ["test_db", "test_table"], "load_detailed_metadata": False} + ) + response = hive_namespace.describe_table(request) + + assert response.location == "/tmp/warehouse/test_db.db/test_table" + def test_deregister_table(self, hive_namespace, mock_hive_client): """Test deregistering a table without deleting data.""" mock_table = MagicMock() diff --git a/python/tests/test_hive3.py b/python/tests/test_hive3.py index d0a235b..60b3fda 100644 --- a/python/tests/test_hive3.py +++ b/python/tests/test_hive3.py @@ -213,6 +213,31 @@ def test_describe_table(self, hive_namespace, mock_hive_client): mock_client_instance.get_table.assert_called_once_with("test_db", "test_table") + def test_describe_table_accepts_dict_request( + self, hive_namespace, mock_hive_client + ): + """pylance's Rust namespace bridge passes the request as a plain dict + (DictWithModelDump), not a DescribeTableRequest model. describe_table must + coerce it instead of raising AttributeError on attribute access. See #181.""" + mock_table = MagicMock() + mock_table.sd.location = "/tmp/warehouse/test_db/test_table" + mock_table.parameters = {"table_type": "lance", "version": "42"} + + mock_client_instance = MagicMock() + mock_client_instance.get_table.return_value = mock_table + mock_hive_client.__enter__.return_value = mock_client_instance + + class DictWithModelDump(dict): + def model_dump(self): + return dict(self) + + request = DictWithModelDump( + {"id": ["hive", "test_db", "test_table"], "load_detailed_metadata": False} + ) + response = hive_namespace.describe_table(request) + + assert response.location == "/tmp/warehouse/test_db/test_table" + def test_deregister_table(self, hive_namespace, mock_hive_client): """Test deregistering a table with 3-level identifier.""" mock_table = MagicMock() diff --git a/python/tests/test_iceberg.py b/python/tests/test_iceberg.py index 67fe2b7..556578c 100644 --- a/python/tests/test_iceberg.py +++ b/python/tests/test_iceberg.py @@ -430,6 +430,40 @@ def test_describe_table(self, mock_rest_client_class): self.assertEqual(response.properties, {"table_type": "lance", "key": "value"}) self.assertFalse(response.managed_versioning) + @patch("lance_namespace_impls.iceberg.RestClient") + def test_describe_table_accepts_dict_request(self, mock_rest_client_class): + """pylance's Rust namespace bridge passes the request as a plain dict + (DictWithModelDump), not a DescribeTableRequest model. describe_table must + coerce it instead of raising AttributeError on attribute access. See #181.""" + mock_client = MagicMock() + mock_rest_client_class.return_value = mock_client + + mock_client.get.side_effect = [ + {"defaults": {"prefix": "warehouse1"}}, + { + "metadata": { + "location": "/data/lance/ns/table", + "properties": {"table_type": "lance", "key": "value"}, + } + }, + ] + + namespace = IcebergNamespace(**self.properties) + + class DictWithModelDump(dict): + def model_dump(self): + return dict(self) + + request = DictWithModelDump( + { + "id": ["warehouse1", "test_namespace", "test_table"], + "load_detailed_metadata": False, + } + ) + response = namespace.describe_table(request) + + self.assertEqual(response.location, "/data/lance/ns/table") + @patch("lance_namespace_impls.iceberg.RestClient") def test_describe_table_not_lance(self, mock_rest_client_class): """Test describing a table that is not a Lance table.""" diff --git a/python/tests/test_polaris.py b/python/tests/test_polaris.py index 20063bb..c32cdbb 100644 --- a/python/tests/test_polaris.py +++ b/python/tests/test_polaris.py @@ -382,6 +382,38 @@ def test_describe_table(self, mock_rest_client_class): "/polaris/v1/test_catalog/namespaces/test_namespace/generic-tables/test_table" ) + @patch("lance_namespace_impls.polaris.RestClient") + def test_describe_table_accepts_dict_request(self, mock_rest_client_class): + """pylance's Rust namespace bridge passes the request as a plain dict + (DictWithModelDump), not a DescribeTableRequest model. describe_table must + coerce it instead of raising AttributeError on attribute access. See #181.""" + mock_client = MagicMock() + mock_rest_client_class.return_value = mock_client + + mock_client.get.return_value = { + "table": { + "format": "lance", + "base-location": "/data/lance/ns/table", + "properties": {"key": "value"}, + } + } + + namespace = PolarisNamespace(**self.properties) + + class DictWithModelDump(dict): + def model_dump(self): + return dict(self) + + request = DictWithModelDump( + { + "id": ["test_catalog", "test_namespace", "test_table"], + "load_detailed_metadata": False, + } + ) + response = namespace.describe_table(request) + + self.assertEqual(response.location, "/data/lance/ns/table") + @patch("lance_namespace_impls.polaris.RestClient") def test_describe_table_not_lance(self, mock_rest_client_class): """Test describing a table that is not a Lance table.""" diff --git a/python/tests/test_unity.py b/python/tests/test_unity.py index fc6e0dc..ef66c28 100644 --- a/python/tests/test_unity.py +++ b/python/tests/test_unity.py @@ -359,6 +359,44 @@ def test_describe_table(self, mock_rest_client_class): response.location, "/data/lance/test_catalog/test_schema/test_table" ) + @patch("lance_namespace_impls.unity.RestClient") + def test_describe_table_accepts_dict_request(self, mock_rest_client_class): + """pylance's Rust namespace bridge passes the request as a plain dict + (DictWithModelDump), not a DescribeTableRequest model. describe_table must + coerce it instead of raising AttributeError on attribute access. See #181.""" + mock_client = MagicMock() + mock_rest_client_class.return_value = mock_client + + mock_table_info = TableInfo( + name="test_table", + catalog_name="test_catalog", + schema_name="test_schema", + table_type="EXTERNAL", + data_source_format="TEXT", + columns=[], + storage_location="/data/lance/test_catalog/test_schema/test_table", + properties={"table_type": "lance"}, + ) + mock_client.get.return_value = mock_table_info + + namespace = UnityNamespace(**self.properties) + + class DictWithModelDump(dict): + def model_dump(self): + return dict(self) + + request = DictWithModelDump( + { + "id": ["test_catalog", "test_schema", "test_table"], + "load_detailed_metadata": False, + } + ) + response = namespace.describe_table(request) + + self.assertEqual( + response.location, "/data/lance/test_catalog/test_schema/test_table" + ) + def test_arrow_type_conversion(self): """Test Arrow type to Unity type conversion.""" namespace = UnityNamespace(**self.properties)