From 79ac3bd1f6da470ed0bf8fd4778841cd373667a7 Mon Sep 17 00:00:00 2001 From: 1fanwang <1fannnw@gmail.com> Date: Thu, 23 Jul 2026 15:22:35 -0700 Subject: [PATCH 1/2] Validate Databricks Repos operators' template fields after rendering branch, tag and repo_path are template fields, rendered after __init__ runs. The Create, Update and Delete Repos operators enforced their mutual-exclusivity and presence combinations in the constructor, acting on the un-rendered Jinja expressions. Move those checks into execute(), preserving order; git_url/git_provider detection reads no template field and stays in __init__. related: #70296 Signed-off-by: 1fanwang <1fannnw@gmail.com> --- .../databricks/operators/databricks_repos.py | 31 ++++++++++--------- .../operators/test_databricks_repos.py | 29 +++++++++-------- .../validate_operators_init_exemptions.txt | 3 -- 3 files changed, 33 insertions(+), 30 deletions(-) diff --git a/providers/databricks/src/airflow/providers/databricks/operators/databricks_repos.py b/providers/databricks/src/airflow/providers/databricks/operators/databricks_repos.py index cb513ea8873ba..862c4944ff7cb 100644 --- a/providers/databricks/src/airflow/providers/databricks/operators/databricks_repos.py +++ b/providers/databricks/src/airflow/providers/databricks/operators/databricks_repos.py @@ -97,8 +97,6 @@ def __init__( else: self.git_provider = git_provider self.repo_path = repo_path - if branch is not None and tag is not None: - raise AirflowException("Only one of branch or tag should be provided, but not both") self.branch = branch self.tag = tag @@ -131,6 +129,9 @@ def execute(self, context: Context): :param context: context :return: Repo ID """ + # branch/tag are template fields; validate after rendering, not in __init__. + if self.branch is not None and self.tag is not None: + raise AirflowException("Only one of branch or tag should be provided, but not both") payload = { "url": self.git_url, "provider": self.git_provider, @@ -225,14 +226,6 @@ def __init__( self.databricks_conn_id = databricks_conn_id self.databricks_retry_limit = databricks_retry_limit self.databricks_retry_delay = databricks_retry_delay - if branch is not None and tag is not None: - raise AirflowException("Only one of branch or tag should be provided, but not both") - if branch is None and tag is None: - raise AirflowException("One of branch or tag should be provided") - if repo_id is not None and repo_path is not None: - raise AirflowException("Only one of repo_id or repo_path should be provided, but not both") - if repo_id is None and repo_path is None: - raise AirflowException("One of repo_id or repo_path should be provided") self.repo_path = repo_path self.repo_id = repo_id self.branch = branch @@ -248,6 +241,15 @@ def _hook(self) -> DatabricksHook: ) def execute(self, context: Context): + # branch/tag/repo_path are template fields; validate after rendering, not in __init__. + if self.branch is not None and self.tag is not None: + raise AirflowException("Only one of branch or tag should be provided, but not both") + if self.branch is None and self.tag is None: + raise AirflowException("One of branch or tag should be provided") + if self.repo_id is not None and self.repo_path is not None: + raise AirflowException("Only one of repo_id or repo_path should be provided, but not both") + if self.repo_id is None and self.repo_path is None: + raise AirflowException("One of repo_id or repo_path should be provided") if self.repo_path is not None: self.repo_id = self._hook.get_repo_by_path(self.repo_path) if self.repo_id is None: @@ -297,10 +299,6 @@ def __init__( self.databricks_conn_id = databricks_conn_id self.databricks_retry_limit = databricks_retry_limit self.databricks_retry_delay = databricks_retry_delay - if repo_id is not None and repo_path is not None: - raise AirflowException("Only one of repo_id or repo_path should be provided, but not both") - if repo_id is None and repo_path is None: - raise AirflowException("One of repo_id repo_path tag should be provided") self.repo_path = repo_path self.repo_id = repo_id @@ -314,6 +312,11 @@ def _hook(self) -> DatabricksHook: ) def execute(self, context: Context): + # repo_path is a template field; validate after rendering, not in __init__. + if self.repo_id is not None and self.repo_path is not None: + raise AirflowException("Only one of repo_id or repo_path should be provided, but not both") + if self.repo_id is None and self.repo_path is None: + raise AirflowException("One of repo_id repo_path tag should be provided") if self.repo_path is not None: self.repo_id = self._hook.get_repo_by_path(self.repo_path) if self.repo_id is None: diff --git a/providers/databricks/tests/unit/databricks/operators/test_databricks_repos.py b/providers/databricks/tests/unit/databricks/operators/test_databricks_repos.py index 398050cf1ff8f..19d0adccf558a 100644 --- a/providers/databricks/tests/unit/databricks/operators/test_databricks_repos.py +++ b/providers/databricks/tests/unit/databricks/operators/test_databricks_repos.py @@ -77,24 +77,26 @@ def test_update_with_path(self, db_mock_class): db_mock.update_repo.assert_called_once_with("123", {"tag": "v1.0.0"}) def test_init_exception(self): - """ - Tests handling of incorrect parameters passed to ``__init__`` - """ + """Incorrect parameter combinations are rejected at execute, after rendering.""" + op = DatabricksReposUpdateOperator(task_id=TASK_ID, repo_id="abc", repo_path="path", branch="abc") with pytest.raises( AirflowException, match="Only one of repo_id or repo_path should be provided, but not both" ): - DatabricksReposUpdateOperator(task_id=TASK_ID, repo_id="abc", repo_path="path", branch="abc") + op.execute(None) + op = DatabricksReposUpdateOperator(task_id=TASK_ID, branch="abc") with pytest.raises(AirflowException, match="One of repo_id or repo_path should be provided"): - DatabricksReposUpdateOperator(task_id=TASK_ID, branch="abc") + op.execute(None) + op = DatabricksReposUpdateOperator(task_id=TASK_ID, repo_id="123", branch="123", tag="123") with pytest.raises( AirflowException, match="Only one of branch or tag should be provided, but not both" ): - DatabricksReposUpdateOperator(task_id=TASK_ID, repo_id="123", branch="123", tag="123") + op.execute(None) + op = DatabricksReposUpdateOperator(task_id=TASK_ID, repo_id="123") with pytest.raises(AirflowException, match="One of branch or tag should be provided"): - DatabricksReposUpdateOperator(task_id=TASK_ID, repo_id="123") + op.execute(None) class TestDatabricksReposDeleteOperator: @@ -140,16 +142,16 @@ def test_delete_with_path(self, db_mock_class): db_mock.delete_repo.assert_called_once_with("123") def test_init_exception(self): - """ - Tests handling of incorrect parameters passed to ``__init__`` - """ + """Incorrect parameter combinations are rejected at execute, after rendering.""" + op = DatabricksReposDeleteOperator(task_id=TASK_ID, repo_id="abc", repo_path="path") with pytest.raises( AirflowException, match="Only one of repo_id or repo_path should be provided, but not both" ): - DatabricksReposDeleteOperator(task_id=TASK_ID, repo_id="abc", repo_path="path") + op.execute(None) + op = DatabricksReposDeleteOperator(task_id=TASK_ID) with pytest.raises(AirflowException, match="One of repo_id repo_path tag should be provided"): - DatabricksReposDeleteOperator(task_id=TASK_ID) + op.execute(None) class TestDatabricksReposCreateOperator: @@ -286,7 +288,8 @@ def test_init_exception(self): with pytest.raises(AirflowException, match=exception_message): op.execute(None) + op = DatabricksReposCreateOperator(task_id=TASK_ID, git_url=git_url, branch="123", tag="123") with pytest.raises( AirflowException, match="Only one of branch or tag should be provided, but not both" ): - DatabricksReposCreateOperator(task_id=TASK_ID, git_url=git_url, branch="123", tag="123") + op.execute(None) diff --git a/scripts/ci/prek/validate_operators_init_exemptions.txt b/scripts/ci/prek/validate_operators_init_exemptions.txt index 01fd5bc56dbb7..606be94f600d8 100644 --- a/scripts/ci/prek/validate_operators_init_exemptions.txt +++ b/scripts/ci/prek/validate_operators_init_exemptions.txt @@ -36,9 +36,6 @@ providers/cncf/kubernetes/src/airflow/providers/cncf/kubernetes/operators/resour providers/cohere/src/airflow/providers/cohere/operators/embedding.py::CohereEmbeddingOperator providers/common/ai/src/airflow/providers/common/ai/operators/agent.py::AgentOperator providers/common/ai/src/airflow/providers/common/ai/operators/document_loader.py::DocumentLoaderOperator -providers/databricks/src/airflow/providers/databricks/operators/databricks_repos.py::DatabricksReposCreateOperator -providers/databricks/src/airflow/providers/databricks/operators/databricks_repos.py::DatabricksReposDeleteOperator -providers/databricks/src/airflow/providers/databricks/operators/databricks_repos.py::DatabricksReposUpdateOperator providers/databricks/src/airflow/providers/databricks/operators/databricks_sql.py::DatabricksCopyIntoOperator providers/databricks/src/airflow/providers/databricks/sensors/databricks.py::DatabricksSQLStatementsSensor providers/dbt/cloud/src/airflow/providers/dbt/cloud/operators/dbt.py::DbtCloudGetJobRunArtifactOperator From 6998d4aad5c799053af62779729180dac3f90443 Mon Sep 17 00:00:00 2001 From: 1fanwang <1fannnw@gmail.com> Date: Fri, 24 Jul 2026 11:31:30 -0700 Subject: [PATCH 2/2] Drop narrating comments from Databricks Repos operators Signed-off-by: 1fanwang <1fannnw@gmail.com> --- .../airflow/providers/databricks/operators/databricks_repos.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/providers/databricks/src/airflow/providers/databricks/operators/databricks_repos.py b/providers/databricks/src/airflow/providers/databricks/operators/databricks_repos.py index 862c4944ff7cb..5614fd403dd5f 100644 --- a/providers/databricks/src/airflow/providers/databricks/operators/databricks_repos.py +++ b/providers/databricks/src/airflow/providers/databricks/operators/databricks_repos.py @@ -129,7 +129,6 @@ def execute(self, context: Context): :param context: context :return: Repo ID """ - # branch/tag are template fields; validate after rendering, not in __init__. if self.branch is not None and self.tag is not None: raise AirflowException("Only one of branch or tag should be provided, but not both") payload = { @@ -241,7 +240,6 @@ def _hook(self) -> DatabricksHook: ) def execute(self, context: Context): - # branch/tag/repo_path are template fields; validate after rendering, not in __init__. if self.branch is not None and self.tag is not None: raise AirflowException("Only one of branch or tag should be provided, but not both") if self.branch is None and self.tag is None: @@ -312,7 +310,6 @@ def _hook(self) -> DatabricksHook: ) def execute(self, context: Context): - # repo_path is a template field; validate after rendering, not in __init__. if self.repo_id is not None and self.repo_path is not None: raise AirflowException("Only one of repo_id or repo_path should be provided, but not both") if self.repo_id is None and self.repo_path is None: