This is an automated email from the ASF dual-hosted git repository.
shahar1 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/main by this push:
new 734e8b3d70d Emit GCSToBigQueryOperator deprecation warning after
rendering (#70542)
734e8b3d70d is described below
commit 734e8b3d70d909bab59b21a90c692a28fadfc17d
Author: Yuseok Jo <[email protected]>
AuthorDate: Sat Sep 12 18:16:45 2026 +0900
Emit GCSToBigQueryOperator deprecation warning after rendering (#70542)
---
.../google/cloud/transfers/gcs_to_bigquery.py | 38 +++---
.../google/cloud/transfers/test_gcs_to_bigquery.py | 135 ++++++++++++---------
.../ci/prek/validate_operators_init_exemptions.txt | 1 -
3 files changed, 104 insertions(+), 70 deletions(-)
diff --git
a/providers/google/src/airflow/providers/google/cloud/transfers/gcs_to_bigquery.py
b/providers/google/src/airflow/providers/google/cloud/transfers/gcs_to_bigquery.py
index 68a137d7d05..549f405e0fa 100644
---
a/providers/google/src/airflow/providers/google/cloud/transfers/gcs_to_bigquery.py
+++
b/providers/google/src/airflow/providers/google/cloud/transfers/gcs_to_bigquery.py
@@ -261,8 +261,6 @@ class GCSToBigQueryOperator(BaseOperator):
self.configuration: dict[str, Any] = {}
# GCS config
- if src_fmt_configs is None:
- src_fmt_configs = {}
if time_partitioning is None:
time_partitioning = {}
if range_partitioning is None:
@@ -272,9 +270,6 @@ class GCSToBigQueryOperator(BaseOperator):
self.bucket = bucket
self.source_objects = source_objects
self.schema_object = schema_object
-
- if schema_object_bucket is None:
- schema_object_bucket = bucket
self.schema_object_bucket = schema_object_bucket
# BQ config
@@ -305,16 +300,6 @@ class GCSToBigQueryOperator(BaseOperator):
self.schema_update_options = schema_update_options
self.src_fmt_configs = src_fmt_configs
- if src_fmt_configs:
- warnings.warn(
- "The 'src_fmt_configs' parameter is deprecated. Use
'extra_config' instead. "
- "Note: 'extra_config' uses the fully-nested API structure, so
format-specific "
- "options must be nested under their parent key "
- "(e.g., {'parquetOptions': {'enableListInference': True}}
rather than "
- "{'enableListInference': True}).",
- AirflowProviderDeprecationWarning,
- stacklevel=2,
- )
self.extra_config = extra_config
self.time_partitioning = time_partitioning
self.range_partitioning = range_partitioning
@@ -359,7 +344,30 @@ class GCSToBigQueryOperator(BaseOperator):
if job.error_result:
raise AirflowException(f"BigQuery job {job.job_id} failed:
{job.error_result}")
+ def _warn_on_deprecated_template_fields(self) -> None:
+ if self.src_fmt_configs:
+ warnings.warn(
+ "The 'src_fmt_configs' parameter is deprecated. Use
'extra_config' instead. "
+ "Note: 'extra_config' uses the fully-nested API structure, so
format-specific "
+ "options must be nested under their parent key "
+ "(e.g., {'parquetOptions': {'enableListInference': True}}
rather than "
+ "{'enableListInference': True}).",
+ AirflowProviderDeprecationWarning,
+ stacklevel=1,
+ )
+
def execute(self, context: Context):
+ # Template fields render after __init__, so defaults that depend on a
template field
+ # (schema_object_bucket falls back to bucket) and the src_fmt_configs
deprecation check
+ # must run here, against the rendered values.
+ if self.src_fmt_configs is None:
+ self.src_fmt_configs = {}
+ if self.schema_object_bucket is None:
+ self.schema_object_bucket = self.bucket
+ # Not captured in the rendered-template view (it defaults after
rendering), so log it.
+ self.log.info("schema_object_bucket not set, defaulting to bucket
%s", self.bucket)
+ self._warn_on_deprecated_template_fields()
+
hook = BigQueryHook(
gcp_conn_id=self.gcp_conn_id,
location=self.location,
diff --git
a/providers/google/tests/unit/google/cloud/transfers/test_gcs_to_bigquery.py
b/providers/google/tests/unit/google/cloud/transfers/test_gcs_to_bigquery.py
index b71456dd658..18007fff91a 100644
--- a/providers/google/tests/unit/google/cloud/transfers/test_gcs_to_bigquery.py
+++ b/providers/google/tests/unit/google/cloud/transfers/test_gcs_to_bigquery.py
@@ -812,6 +812,33 @@ class TestGCSToBigQueryOperator:
)
gcs_hook.return_value.download.assert_called_once_with(SCHEMA_BUCKET,
SCHEMA_OBJECT)
+ @mock.patch(GCS_TO_BQ_PATH.format("GCSHook"))
+ @mock.patch(GCS_TO_BQ_PATH.format("BigQueryHook"))
+ def test_schema_object_bucket_defaults_to_bucket_when_omitted(self,
bq_hook, gcs_hook):
+ # The schema_object_bucket -> bucket fallback now runs in execute()
after rendering; when
+ # schema_object_bucket is omitted the schema download must still
target the bucket.
+ bq_hook.return_value.insert_job.side_effect = [
+ MagicMock(job_id=REAL_JOB_ID, error_result=False),
+ REAL_JOB_ID,
+ ]
+ bq_hook.return_value.generate_job_id.return_value = REAL_JOB_ID
+ bq_hook.return_value.split_tablename.return_value = (PROJECT_ID,
DATASET, TABLE)
+ gcs_hook.return_value.download.return_value =
bytes(json.dumps(SCHEMA_FIELDS), "utf-8")
+ operator = GCSToBigQueryOperator(
+ task_id=TASK_ID,
+ bucket=TEST_BUCKET,
+ source_objects=TEST_SOURCE_OBJECTS,
+ schema_object=SCHEMA_OBJECT,
+ write_disposition=WRITE_DISPOSITION,
+ destination_project_dataset_table=TEST_EXPLICIT_DEST,
+ external_table=True,
+ project_id=JOB_PROJECT_ID,
+ )
+
+ operator.execute(context=MagicMock())
+
+ gcs_hook.return_value.download.assert_called_once_with(TEST_BUCKET,
SCHEMA_OBJECT)
+
@mock.patch(GCS_TO_BQ_PATH.format("GCSHook"))
@mock.patch(GCS_TO_BQ_PATH.format("BigQueryHook"))
def
test_schema_obj_without_external_table_should_execute_successfully(self,
bq_hook, gcs_hook):
@@ -1742,23 +1769,23 @@ class TestGCSToBigQueryOperator:
hook.return_value.generate_job_id.return_value = REAL_JOB_ID
hook.return_value.split_tablename.return_value = (PROJECT_ID, DATASET,
TABLE)
- with pytest.warns(AirflowProviderDeprecationWarning,
match="src_fmt_configs"):
- operator = GCSToBigQueryOperator(
- task_id=TASK_ID,
- bucket=TEST_BUCKET,
- source_objects=TEST_SOURCE_OBJECTS,
- destination_project_dataset_table=TEST_EXPLICIT_DEST,
- schema_fields=SCHEMA_FIELDS,
- write_disposition=WRITE_DISPOSITION,
- external_table=True,
- project_id=JOB_PROJECT_ID,
- source_format="PARQUET",
- src_fmt_configs={
- "enableListInference": True,
- },
- )
+ operator = GCSToBigQueryOperator(
+ task_id=TASK_ID,
+ bucket=TEST_BUCKET,
+ source_objects=TEST_SOURCE_OBJECTS,
+ destination_project_dataset_table=TEST_EXPLICIT_DEST,
+ schema_fields=SCHEMA_FIELDS,
+ write_disposition=WRITE_DISPOSITION,
+ external_table=True,
+ project_id=JOB_PROJECT_ID,
+ source_format="PARQUET",
+ src_fmt_configs={
+ "enableListInference": True,
+ },
+ )
- operator.execute(context=MagicMock())
+ with pytest.warns(AirflowProviderDeprecationWarning,
match="src_fmt_configs"):
+ operator.execute(context=MagicMock())
hook.return_value.create_table.assert_called_once_with(
exists_ok=True,
@@ -1845,22 +1872,22 @@ class TestGCSToBigQueryOperator:
]
hook.return_value.generate_job_id.return_value = REAL_JOB_ID
hook.return_value.split_tablename.return_value = (PROJECT_ID, DATASET,
TABLE)
- with pytest.warns(AirflowProviderDeprecationWarning,
match="src_fmt_configs"):
- operator = GCSToBigQueryOperator(
- task_id=TASK_ID,
- bucket=TEST_BUCKET,
- source_objects=TEST_SOURCE_OBJECTS,
- write_disposition=WRITE_DISPOSITION,
- destination_project_dataset_table=TEST_EXPLICIT_DEST,
- external_table=False,
- project_id=JOB_PROJECT_ID,
- source_format="PARQUET",
- src_fmt_configs={
- "enableListInference": True,
- },
- )
+ operator = GCSToBigQueryOperator(
+ task_id=TASK_ID,
+ bucket=TEST_BUCKET,
+ source_objects=TEST_SOURCE_OBJECTS,
+ write_disposition=WRITE_DISPOSITION,
+ destination_project_dataset_table=TEST_EXPLICIT_DEST,
+ external_table=False,
+ project_id=JOB_PROJECT_ID,
+ source_format="PARQUET",
+ src_fmt_configs={
+ "enableListInference": True,
+ },
+ )
- operator.execute(context=MagicMock())
+ with pytest.warns(AirflowProviderDeprecationWarning,
match="src_fmt_configs"):
+ operator.execute(context=MagicMock())
calls = [
call(
@@ -2054,18 +2081,18 @@ class TestGCSToBigQueryOperator:
hook.return_value.generate_job_id.return_value = REAL_JOB_ID
hook.return_value.split_tablename.return_value = (PROJECT_ID, DATASET,
TABLE)
- with pytest.warns(AirflowProviderDeprecationWarning,
match="src_fmt_configs"):
- operator = GCSToBigQueryOperator(
- task_id=TASK_ID,
- bucket=TEST_BUCKET,
- source_objects=TEST_SOURCE_OBJECTS,
- destination_project_dataset_table=TEST_EXPLICIT_DEST,
- write_disposition=WRITE_DISPOSITION,
- project_id=JOB_PROJECT_ID,
- src_fmt_configs={"skipLeadingRows": 1},
- )
+ operator = GCSToBigQueryOperator(
+ task_id=TASK_ID,
+ bucket=TEST_BUCKET,
+ source_objects=TEST_SOURCE_OBJECTS,
+ destination_project_dataset_table=TEST_EXPLICIT_DEST,
+ write_disposition=WRITE_DISPOSITION,
+ project_id=JOB_PROJECT_ID,
+ src_fmt_configs={"skipLeadingRows": 1},
+ )
- operator.execute(context=MagicMock())
+ with pytest.warns(AirflowProviderDeprecationWarning,
match="src_fmt_configs"):
+ operator.execute(context=MagicMock())
config = hook.return_value.insert_job.call_args[1]["configuration"]
assert config["load"]["skipLeadingRows"] == 1
@@ -2076,19 +2103,19 @@ class TestGCSToBigQueryOperator:
hook.return_value.generate_job_id.return_value = REAL_JOB_ID
hook.return_value.split_tablename.return_value = (PROJECT_ID, DATASET,
TABLE)
- with pytest.warns(AirflowProviderDeprecationWarning,
match="src_fmt_configs"):
- operator = GCSToBigQueryOperator(
- task_id=TASK_ID,
- bucket=TEST_BUCKET,
- source_objects=TEST_SOURCE_OBJECTS,
- destination_project_dataset_table=TEST_EXPLICIT_DEST,
- write_disposition=WRITE_DISPOSITION,
- project_id=JOB_PROJECT_ID,
- src_fmt_configs={"skipLeadingRows": 1},
- extra_config={"skipLeadingRows": 5, "columnNameCharacterMap":
"STRICT"},
- )
+ operator = GCSToBigQueryOperator(
+ task_id=TASK_ID,
+ bucket=TEST_BUCKET,
+ source_objects=TEST_SOURCE_OBJECTS,
+ destination_project_dataset_table=TEST_EXPLICIT_DEST,
+ write_disposition=WRITE_DISPOSITION,
+ project_id=JOB_PROJECT_ID,
+ src_fmt_configs={"skipLeadingRows": 1},
+ extra_config={"skipLeadingRows": 5, "columnNameCharacterMap":
"STRICT"},
+ )
- operator.execute(context=MagicMock())
+ with pytest.warns(AirflowProviderDeprecationWarning,
match="src_fmt_configs"):
+ operator.execute(context=MagicMock())
config = hook.return_value.insert_job.call_args[1]["configuration"]
# extra_config wins for overlapping key
diff --git a/scripts/ci/prek/validate_operators_init_exemptions.txt
b/scripts/ci/prek/validate_operators_init_exemptions.txt
index af75c526a1d..25cb0fe1a14 100644
--- a/scripts/ci/prek/validate_operators_init_exemptions.txt
+++ b/scripts/ci/prek/validate_operators_init_exemptions.txt
@@ -18,5 +18,4 @@
providers/google/src/airflow/providers/google/cloud/operators/gcs.py::GCSFileTra
providers/google/src/airflow/providers/google/cloud/sensors/bigquery_dts.py::BigQueryDataTransferServiceTransferRunSensor
providers/google/src/airflow/providers/google/cloud/sensors/cloud_composer.py::CloudComposerExternalTaskSensor
providers/google/src/airflow/providers/google/cloud/transfers/azure_fileshare_to_gcs.py::AzureFileShareToGCSOperator
-providers/google/src/airflow/providers/google/cloud/transfers/gcs_to_bigquery.py::GCSToBigQueryOperator
providers/microsoft/psrp/src/airflow/providers/microsoft/psrp/operators/psrp.py::PsrpOperator