Repository: googleapis/python-bigquery
Pull request #2074 (2024-11-19 19:59:06)

feat: adds two from_api_tests: StorageDesc & SerDeInfo
Adds a test to each of the test sets for:

StorageDescriptor: test_from_api_repr
SerDeInfo: test_from_api_repr



conventional-commit-lint-gcf[bot]: <!-- probot comment [1356997]-->
🤖 I detect that the PR title and the commit message differ and there's only one commit. To use the PR title for the commit history, you can use Github's automerge feature with squashing, or use `automerge` label. Good luck human!

 -- conventional-commit-lint bot
https://conventionalcommits.org/

Patch:
diff --git a/google/cloud/bigquery/external_config.py b/google/cloud/bigquery/external_config.py
index 59b71599..8d66f834 100644
--- a/google/cloud/bigquery/external_config.py
+++ b/google/cloud/bigquery/external_config.py
@@ -1027,7 +1027,7 @@ class ExternalCatalogDatasetOptions:
         default_storage_location_uri: Optional[str] = None,
         parameters: Optional[Dict[str, Any]] = None,
     ):
-        self._properties = {}
+        self._properties: Dict[str, Any] = {}
         self.default_storage_location_uri = default_storage_location_uri
         self.parameters = parameters
 
@@ -1168,6 +1168,7 @@ class ExternalCatalogTableOptions:
             Dict[str, Any]:
                 A dictionary in the format used by the BigQuery API.
         """
+
         config = copy.deepcopy(self._properties)
         return config
 
@@ -1186,3 +1187,6 @@ class ExternalCatalogTableOptions:
         config = cls()
         config._properties = copy.deepcopy(resource)
         return config
+
+    def __eq__(self, value):
+        return self.to_api_repr() == value.to_api_repr()
diff --git a/google/cloud/bigquery/schema.py b/google/cloud/bigquery/schema.py
index dc624121..825e9bc5 100644
--- a/google/cloud/bigquery/schema.py
+++ b/google/cloud/bigquery/schema.py
@@ -610,7 +610,7 @@ class TableSchema:
     def __init__(
         self, fields: Optional[list] = None, foreign_type_info: Optional[str] = None
     ):
-        self._properties = {}
+        self._properties: Dict[str, Any] = {}
         self.fields = fields
         self.foreign_type_info = foreign_type_info
 
@@ -621,7 +621,7 @@ class TableSchema:
         return self._properties.get("fields")
 
     @fields.setter
-    def fields(self, value: list, dtype: str) -> str:
+    def fields(self, value: list, dtype: str) -> None:
         value = _isinstance_or_raise(value, list, none_allowed=True)
         self._properties["fields"] = value
 
@@ -633,7 +633,7 @@ class TableSchema:
         return self._properties.get("foreignTypeInfo")
 
     @foreign_type_info.setter
-    def foreign_type_info(self, value: str, dtype: str) -> str:
+    def foreign_type_info(self, value: str, dtype: str) -> None:
         if not isinstance(value, str):
             raise ValueError(
                 f"Pass {value} as a '{repr(dtype)}'." f"Got {type(value)}."
@@ -701,8 +701,8 @@ class StorageDescriptor:
             "org.apache.hadoop.hive.ql.io.orc.OrcInputFormat"). The maximum
             length is 128 characters.
         locationUri (Optional[str]): The physical location of the table (e.g.
-            `gs://spark-dataproc-data/pangea-data/case_sensitive/` or
-            `gs://spark-dataproc-data/pangea-data/*`). The maximum length is
+            'gs://spark-dataproc-data/pangea-data/case_sensitive/' or
+            'gs://spark-dataproc-data/pangea-data/'). The maximum length is
             2056 bytes.
         outputFormat (Optional[str]): Specifies the fully qualified class name
             of the OutputFormat (e.g.
@@ -718,7 +718,7 @@ class StorageDescriptor:
         output_format: Optional[str] = None,
         serde_info: Optional[SerDeInfo] = None,
     ):
-        self._properties = {}
+        self._properties: Dict[str, Any] = {}
         self.input_format = input_format
         self.location_uri = location_uri
         self.output_format = output_format
@@ -739,9 +739,9 @@ class StorageDescriptor:
 
     @property
     def location_uri(self) -> Any:
-        """Optional. The physical location of the table (e.g. `gs://spark-
-        dataproc-data/pangea-data/case_sensitive/` or `gs://spark-dataproc-
-        data/pangea-data/*`). The maximum length is 2056 bytes."""
+        """Optional. The physical location of the table (e.g. 'gs://spark-
+        dataproc-data/pangea-data/case_sensitive/' or 'gs://spark-dataproc-
+        data/pangea-data/'). The maximum length is 2056 bytes."""
 
         return self._properties.get("locationUri")
 
@@ -768,9 +768,9 @@ class StorageDescriptor:
         """Optional. Serializer and deserializer information."""
 
         prop = _get_sub_prop(self._properties, ["serDeInfo"])
-        print(f"DINOSAUR in SD: {prop}\n\n{self._properties}")
         if prop is not None:
-            prop = SerDeInfo().from_api_repr(prop)
+            prop = StorageDescriptor().from_api_repr(prop)
+            print(f"DINOSAUR prop: {prop}")
 
         return prop
 
@@ -829,7 +829,7 @@ class SerDeInfo:
         name: Optional[str] = None,
         parameters: Optional[dict[str, str]] = None,
     ):
-        self._properties = {}
+        self._properties: Dict[str, Any] = {}
         self.serialization_library = serialization_library
         self.name = name
         self.parameters = parameters
@@ -892,6 +892,6 @@ class SerDeInfo:
         Returns:
             An instance of the class initialized with data from 'resource'.
         """
-        config = cls()
+        config = cls("")
         config._properties = copy.deepcopy(resource)
         return config


Test patch:
diff --git a/tests/unit/test_schema.py b/tests/unit/test_schema.py
index ee0538b3..769f7efc 100644
--- a/tests/unit/test_schema.py
+++ b/tests/unit/test_schema.py
@@ -1161,6 +1161,23 @@ class TestForeignTypeInfo:
         assert result.to_api_repr() == expected
 
 
+@pytest.fixture
+def _make_storage_descriptor():
+    serdeinfo = SerDeInfo(
+        serialization_library="testpath.to.LazySimpleSerDe",
+        name="serde_lib_name",
+        parameters={"key": "value"},
+    )
+
+    obj = StorageDescriptor(
+        input_format="testpath.to.OrcInputFormat",
+        location_uri="gs://test/path/",
+        output_format="testpath.to.OrcOutputFormat",
+        serde_info=serdeinfo,
+    )
+    return obj
+
+
 class TestStorageDescriptor:
     """Tests for the StorageDescriptor class."""
 
@@ -1251,7 +1268,34 @@ class TestStorageDescriptor:
 
         assert storage_descriptor.to_api_repr() == expected_repr
 
-    # TODO: needs a from_api_repr() test.
+    SERDEINFO = SerDeInfo(
+        serialization_library="testpath.to.LazySimpleSerDe",
+        name="serde_lib_name",
+        parameters={"key": "value"},
+    )
+
+    API_REPR = {
+        "inputFormat": "testpath.to.OrcInputFormat",
+        "locationUri": "gs://test/path/",
+        "outputFormat": "testpath.to.OrcOutputFormat",
+        "serDeInfo": SERDEINFO.to_api_repr(),
+    }
+
+    def test_from_api_repr(self, _make_storage_descriptor):
+        """GIVEN an api representation of a StorageDescriptor (i.e. API_REPR)
+        WHEN converted into a StorageDescriptor using from_api_repr() and
+        displayed as a dict
+        THEN it will have the same representation a StorageDescriptor created
+        directly (via the fixture) and displayed as a dict.
+        """
+        # generate via fixture
+        expected = _make_storage_descriptor
+        resource = self.API_REPR
+        klass = self._get_target_class()
+        # generate via API_REPR
+        result = klass.from_api_repr(resource)
+
+        assert result.to_api_repr() == expected.to_api_repr()
 
 
 class TestSerDeInfo:
@@ -1315,4 +1359,26 @@ class TestSerDeInfo:
         }
         assert serde_info.to_api_repr() == expected_repr
 
-    # TODO: needs a from_api_repr() test.
+    def test_from_api_repr(self, _make_storage_descriptor):
+        """GIVEN an api representation of a SerDeInfo object (i.e. resource)
+        WHEN converted into a SerDeInfo using from_api_repr() and
+        displayed as a dict
+        THEN it will have the same representation a SerDeInfo object created
+        directly (via _make_one()) and displayed as a dict.
+        """
+        resource = {
+            "serializationLibrary": "testpath.to.LazySimpleSerDe",
+            "name": "serde_name",
+            "parameters": {"key": "value"},
+        }
+
+        expected = self._make_one(
+            serialization_library="testpath.to.LazySimpleSerDe",
+            name="serde_name",
+            parameters={"key": "value"},
+        )
+
+        klass = self._get_target_class()
+        result = klass.from_api_repr(resource)
+
+        assert result.to_api_repr() == expected.to_api_repr()
diff --git a/tests/unit/test_table.py b/tests/unit/test_table.py
index dafeb9aa..22251d03 100644
--- a/tests/unit/test_table.py
+++ b/tests/unit/test_table.py
@@ -5880,6 +5880,7 @@ class TestExternalCatalogTableOptions:
         dataset = DatasetReference(self.PROJECT, self.DS_ID)
         table_ref = dataset.table(self.TABLE_NAME)
         table = self._make_one(table_ref)
+        expected = external_catalog_table_options
 
         # Confirm that external catalog table options have not been set
         assert table.external_catalog_table_options is None
@@ -5887,27 +5888,15 @@ class TestExternalCatalogTableOptions:
         # Add an ExternalCatalogTableOptions object to the table.
         table._properties[
             "externalCatalogTableOptions"
-        ] = external_catalog_table_options
-        table_repr = table.to_api_repr()
+        ] = external_catalog_table_options.to_api_repr()
 
         # Extract the ecto object.
-        ecto_output = table_repr["externalCatalogTableOptions"]
+        result = table.external_catalog_table_options
 
         # Confirm that external catalog table options are an
         # ExternalCatalogTableOptions object
-        assert isinstance(ecto_output, ExternalCatalogTableOptions)
-
-        storage_descriptor = request.getfixturevalue("_make_storage_descriptor")
-
-        expected = {
-            "connectionId": "connection123",
-            "parameters": {"key": "value"},
-            "storageDescriptor": storage_descriptor.to_api_repr(),
-        }
-        result = ecto_output.to_api_repr()
-
-        # Confirm that the api_repr of the ecto_output matches the inputs
-        print(f"DINOSAUR : {result}\n\n{expected}")
+        assert isinstance(result, ExternalCatalogTableOptions)
+        assert isinstance(expected, ExternalCatalogTableOptions)
         assert result == expected
 
     def test_external_catalog_table_options_setter(