Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -171,12 +171,9 @@ def __init__(
self.gcp_conn_id = gcp_conn_id
self.api_version = api_version
self.zip_path = zip_path
self.zip_path_preprocessor = ZipPathPreprocessor(body, zip_path)
self.validate_body = validate_body
self._field_validator: GcpBodyFieldValidator | None = None
self.impersonation_chain = impersonation_chain
if validate_body:
self._field_validator = GcpBodyFieldValidator(CLOUD_FUNCTION_VALIDATION, api_version=api_version)
self._validate_inputs()
super().__init__(**kwargs)

def _validate_inputs(self) -> None:
Expand Down Expand Up @@ -227,6 +224,12 @@ def extra_links_params(self) -> dict[str, Any]:
}

def execute(self, context: Context):
self.zip_path_preprocessor = ZipPathPreprocessor(self.body, self.zip_path)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

self.zip_path_preprocessor is now only assigned here, so between construction and the first execute the attribute doesn't exist at all — any access raises AttributeError rather than returning something sensible. That affects subclasses, tests, and anything introspecting the operator between parse and run.

Note you've already handled the sibling case correctly: self._field_validator: GcpBodyFieldValidator | None = None is still initialised in __init__ and merely reassigned in execute. Doing the same for the preprocessor — declare it as None in __init__, build it here — would make the two consistent and keep the operator's attribute surface stable from construction onwards.


Drafted-by: Claude Code (Opus 5); reviewed by @potiuk before posting

if self.validate_body:
self._field_validator = GcpBodyFieldValidator(
CLOUD_FUNCTION_VALIDATION, api_version=self.api_version
)
self._validate_inputs()
hook = CloudFunctionsHook(
gcp_conn_id=self.gcp_conn_id,
api_version=self.api_version,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,25 @@ def test_missing_fields(self, mock_hook, body, message):
op.execute(None)

def test_body_empty(self):
op = CloudFunctionDeployFunctionOperator(
project_id="test_project_id", location="test_region", body={}, task_id="id"
)
with pytest.raises(AirflowException):
CloudFunctionDeployFunctionOperator(
project_id="test_project_id", location="test_region", body={}, task_id="id"
)
op.execute(None)

@mock.patch("airflow.providers.google.cloud.operators.functions.CloudFunctionsHook")
def test_templated_body_deploys_after_rendering(self, mock_hook):
mock_hook.return_value.get_function.side_effect = mock.Mock(
side_effect=HttpError(resp=MOCK_RESP_404, content=b"not found")
)
mock_hook.return_value.create_new_function.return_value = True
op = CloudFunctionDeployFunctionOperator(
project_id=GCP_PROJECT_ID, location=GCP_LOCATION, body="{{ var.value.body }}", task_id="id"
)
# Template rendering replaces the Jinja expression with the resolved value before execute.
op.body = deepcopy(VALID_BODY)
op.execute(context=mock.MagicMock())
mock_hook.return_value.create_new_function.assert_called_once()

@mock.patch("airflow.providers.google.cloud.operators.functions.CloudFunctionsHook")
def test_deploy_execute(self, mock_hook):
Expand Down Expand Up @@ -154,19 +169,21 @@ def test_empty_project_id_is_ok(self, mock_hook):

@mock.patch("airflow.providers.google.cloud.operators.functions.CloudFunctionsHook")
def test_empty_location(self, mock_hook):
op = CloudFunctionDeployFunctionOperator(
project_id="test_project_id", location="", body=None, task_id="id"
)
with pytest.raises(AirflowException) as ctx:
CloudFunctionDeployFunctionOperator(
project_id="test_project_id", location="", body=None, task_id="id"
)
op.execute(None)
err = ctx.value
assert "The required parameter 'location' is missing" in str(err)

@mock.patch("airflow.providers.google.cloud.operators.functions.CloudFunctionsHook")
def test_empty_body(self, mock_hook):
op = CloudFunctionDeployFunctionOperator(
project_id="test_project_id", location="test_region", body=None, task_id="id"
)
with pytest.raises(AirflowException) as ctx:
CloudFunctionDeployFunctionOperator(
project_id="test_project_id", location="test_region", body=None, task_id="id"
)
op.execute(None)
err = ctx.value
assert "The required parameter 'body' is missing" in str(err)

Expand Down Expand Up @@ -381,20 +398,21 @@ def test_invalid_source_code_union_field__execute(self, source_code, message):
),
],
)
def test_invalid_source_code_union_field__init(self, source_code, message):
def test_invalid_source_code_union_field__preprocess(self, source_code, message):
body = deepcopy(VALID_BODY)
body.pop("sourceUploadUrl", None)
body.pop("sourceArchiveUrl", None)
zip_path = source_code.pop("zip_path", None)
body.update(source_code)
op = CloudFunctionDeployFunctionOperator(
project_id="test_project_id",
location="test_region",
body=body,
task_id="id",
zip_path=zip_path,
)
with pytest.raises(AirflowException, match=message):
CloudFunctionDeployFunctionOperator(
project_id="test_project_id",
location="test_region",
body=body,
task_id="id",
zip_path=zip_path,
)
op.execute(None)

@pytest.mark.parametrize(
("source_code", "project_id"),
Expand Down
1 change: 0 additions & 1 deletion scripts/ci/prek/validate_operators_init_exemptions.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ providers/google/src/airflow/providers/google/cloud/operators/cloud_build.py::Cl
providers/google/src/airflow/providers/google/cloud/operators/cloud_storage_transfer_service.py::CloudDataTransferServiceCreateJobOperator
providers/google/src/airflow/providers/google/cloud/operators/dataproc.py::DataprocCreateClusterOperator
providers/google/src/airflow/providers/google/cloud/operators/dataproc.py::DataprocSubmitJobOperator
providers/google/src/airflow/providers/google/cloud/operators/functions.py::CloudFunctionDeployFunctionOperator
providers/google/src/airflow/providers/google/cloud/operators/gcs.py::GCSFileTransformOperator
providers/google/src/airflow/providers/google/cloud/sensors/bigquery_dts.py::BigQueryDataTransferServiceTransferRunSensor
providers/google/src/airflow/providers/google/cloud/sensors/cloud_composer.py::CloudComposerExternalTaskSensor
Expand Down