This is an automated email from the ASF dual-hosted git repository.
vincbeck pushed a commit to branch v3-2-test
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/v3-2-test by this push:
new 54488560a7f Fix FAB roles/users 500 caused by connection hook metadata
mocks (#68421)
54488560a7f is described below
commit 54488560a7f59f1cbb0706bfd38d703a87edfd10
Author: Nishita Matlani <[email protected]>
AuthorDate: Mon Jun 15 09:28:46 2026 -0400
Fix FAB roles/users 500 caused by connection hook metadata mocks (#68421)
---
.../core_api/services/ui/connections.py | 32 ++++++++--------------
.../core_api/services/ui/test_connections.py | 27 ++++++++++++++++++
2 files changed, 39 insertions(+), 20 deletions(-)
diff --git
a/airflow-core/src/airflow/api_fastapi/core_api/services/ui/connections.py
b/airflow-core/src/airflow/api_fastapi/core_api/services/ui/connections.py
index 12e823ce1d8..cd03cb89540 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/services/ui/connections.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/services/ui/connections.py
@@ -27,10 +27,11 @@ from airflow.api_fastapi.core_api.datamodels.connections
import (
ConnectionHookMetaData,
StandardHookFields,
)
+from airflow.providers_manager import HookInfo, ProvidersManager
from airflow.serialization.definitions.param import SerializedParam
if TYPE_CHECKING:
- from airflow.providers_manager import ConnectionFormWidgetInfo, HookInfo
+ from airflow.providers_manager import ConnectionFormWidgetInfo
log = logging.getLogger(__name__)
@@ -126,8 +127,6 @@ class HookMetaService:
"""Get hooks with all details w/o FAB needing to be installed."""
from unittest import mock
- from airflow.providers_manager import ProvidersManager
-
def mock_lazy_gettext(txt: str) -> str:
"""Mock for flask_babel.lazy_gettext."""
return txt
@@ -158,12 +157,7 @@ class HookMetaService:
except ModuleNotFoundError:
sys.modules[mod_name] = MagicMock()
- # We conditionally inject mock classes for missing dependencies
- # to ensure `ProvidersManager` can initialize hook connection widgets
- # without crashing when FAB/WTForms are not installed.
- if "wtforms.StringField" not in sys.modules:
- # Only apply mocks if the actual module wasn't loaded beforehand.
- # This avoids thread-safety issues caused by `unittest.mock.patch`
mutating global states.
+ if "wtforms" not in sys.modules:
with (
mock.patch("wtforms.StringField",
HookMetaService.MockStringField),
mock.patch("wtforms.fields.StringField",
HookMetaService.MockStringField),
@@ -282,19 +276,17 @@ class HookMetaService:
@staticmethod
@cache
def hook_meta_data() -> list[ConnectionHookMetaData]:
- hooks, connection_form_widgets, field_behaviours =
HookMetaService._get_hooks_with_mocked_fab()
- result: list[ConnectionHookMetaData] = []
- widgets =
HookMetaService._convert_extra_fields(connection_form_widgets)
- for hook_key, hook_info in hooks.items():
- if not hook_info:
- continue
- hook_meta = ConnectionHookMetaData(
+ pm = ProvidersManager()
+ hook_items = [(hook_key, hook_info) for hook_key, hook_info in
pm.hooks.items() if hook_info]
+ widgets =
HookMetaService._convert_extra_fields(pm._connection_form_widgets)
+ return [
+ ConnectionHookMetaData(
connection_type=hook_key,
hook_class_name=hook_info.hook_class_name,
- default_conn_name=None, # TODO: later
+ default_conn_name=None,
hook_name=hook_info.hook_name,
-
standard_fields=HookMetaService._make_standard_fields(field_behaviours.get(hook_key)),
+
standard_fields=HookMetaService._make_standard_fields(pm._field_behaviours.get(hook_key)),
extra_fields=widgets.get(hook_key),
)
- result.append(hook_meta)
- return result
+ for hook_key, hook_info in hook_items
+ ]
diff --git
a/airflow-core/tests/unit/api_fastapi/core_api/services/ui/test_connections.py
b/airflow-core/tests/unit/api_fastapi/core_api/services/ui/test_connections.py
index e6ed84778d3..6ff1536ac3d 100644
---
a/airflow-core/tests/unit/api_fastapi/core_api/services/ui/test_connections.py
+++
b/airflow-core/tests/unit/api_fastapi/core_api/services/ui/test_connections.py
@@ -18,6 +18,8 @@ from __future__ import annotations
from airflow.api_fastapi.core_api.services.ui.connections import
HookMetaService
+from tests_common.test_utils.markers import
skip_if_force_lowest_dependencies_marker
+
class TestMockOptional:
def test_mock_optional_is_callable(self):
@@ -30,3 +32,28 @@ class TestMockOptional:
validator = HookMetaService.MockOptional()
result = validator(None, None)
assert result is None
+
+
+class TestHookMetaServiceFabWidgetSafety:
+ @skip_if_force_lowest_dependencies_marker
+ def test_hook_meta_data_does_not_patch_fab_widgets(self):
+ import wtforms # noqa: F401
+ from flask_appbuilder.fieldwidgets import BS3TextFieldWidget as
widget_before
+
+ HookMetaService.hook_meta_data.cache_clear()
+ HookMetaService.hook_meta_data()
+
+ from flask_appbuilder.fieldwidgets import BS3TextFieldWidget as
widget_after
+
+ assert widget_before is widget_after
+
+ @skip_if_force_lowest_dependencies_marker
+ def test_get_hooks_with_mocked_fab_skips_mocks_when_wtforms_loaded(self):
+ import wtforms # noqa: F401
+ from flask_appbuilder.fieldwidgets import BS3TextFieldWidget as
widget_before
+
+ HookMetaService._get_hooks_with_mocked_fab()
+
+ from flask_appbuilder.fieldwidgets import BS3TextFieldWidget as
widget_after
+
+ assert widget_before is widget_after