You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@airflow.apache.org by po...@apache.org on 2022/06/19 18:32:47 UTC

[airflow] branch main updated: Preserve original order of providers' connection extra fields in UI (#24425)

This is an automated email from the ASF dual-hosted git repository.

potiuk 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 9a3e1e47df Preserve original order of providers' connection extra fields in UI (#24425)
9a3e1e47df is described below

commit 9a3e1e47df0614e0b52f4166b8558fe39a57f4fe
Author: Andrey Anshin <An...@taragol.is>
AuthorDate: Sun Jun 19 22:32:27 2022 +0400

    Preserve original order of providers' connection extra fields in UI (#24425)
    
    * Preserve original order of providers' connection extra fields
    
    * Sort widgets before printed in cli command `airflow providers widgets`
---
 airflow/cli/commands/provider_command.py |  2 +-
 airflow/providers_manager.py             | 13 ++++++-
 tests/core/test_providers_manager.py     | 64 ++++++++++++++++++++++++++++++++
 3 files changed, 76 insertions(+), 3 deletions(-)

diff --git a/airflow/cli/commands/provider_command.py b/airflow/cli/commands/provider_command.py
index b89f77fb5d..124c8cea8a 100644
--- a/airflow/cli/commands/provider_command.py
+++ b/airflow/cli/commands/provider_command.py
@@ -82,7 +82,7 @@ def hooks_list(args):
 def connection_form_widget_list(args):
     """Lists all custom connection form fields at the command line"""
     AirflowConsole().print_as(
-        data=list(ProvidersManager().connection_form_widgets.items()),
+        data=list(sorted(ProvidersManager().connection_form_widgets.items())),
         output=args.output,
         mapper=lambda x: {
             "connection_parameter_name": x[0],
diff --git a/airflow/providers_manager.py b/airflow/providers_manager.py
index b5d0297e90..eb7bd9676a 100644
--- a/airflow/providers_manager.py
+++ b/airflow/providers_manager.py
@@ -651,9 +651,15 @@ class ProvidersManager(LoggingMixin):
         """Force-import all hooks and initialize the connections/fields"""
         # Retrieve all hooks to make sure that all of them are imported
         _ = list(self._hooks_lazy_dict.values())
-        self._connection_form_widgets = OrderedDict(sorted(self._connection_form_widgets.items()))
         self._field_behaviours = OrderedDict(sorted(self._field_behaviours.items()))
 
+        # Widgets for connection forms are currently used in two places:
+        # 1. In the UI Connections, expected same order that it defined in Hook.
+        # 2. cli command - `airflow providers widgets` and expected that it in alphabetical order.
+        # It is not possible to recover original ordering after sorting,
+        # that the main reason why original sorting moved to cli part:
+        # self._connection_form_widgets = OrderedDict(sorted(self._connection_form_widgets.items()))
+
     def _discover_taskflow_decorators(self) -> None:
         for name, info in self._provider_dict.items():
             for taskflow_decorator in info.data.get("task-decorators", []):
@@ -900,7 +906,10 @@ class ProvidersManager(LoggingMixin):
 
     @property
     def connection_form_widgets(self) -> Dict[str, ConnectionFormWidgetInfo]:
-        """Returns widgets for connection forms."""
+        """
+        Returns widgets for connection forms.
+        Dictionary keys in the same order that it defined in Hook.
+        """
         self.initialize_providers_hooks()
         self._import_info_from_all_hooks()
         return self._connection_form_widgets
diff --git a/tests/core/test_providers_manager.py b/tests/core/test_providers_manager.py
index a98dc9534e..857ed3f15c 100644
--- a/tests/core/test_providers_manager.py
+++ b/tests/core/test_providers_manager.py
@@ -188,6 +188,70 @@ class TestProviderManager:
         )
         assert provider_manager.connection_form_widgets['extra__test__my_param'].field == widget_field
 
+    def test_connection_form_widgets_fields_order(self):
+        """Check that order of connection for widgets preserved by original Hook order."""
+        test_conn_type = 'test'
+        field_prefix = f'extra__{test_conn_type}__'
+        field_names = ("yyy_param", "aaa_param", "000_param", "foo", "bar", "spam", "egg")
+
+        expected_field_names_order = tuple(f"{field_prefix}{f}" for f in field_names)
+
+        class TestHook:
+            conn_type = test_conn_type
+
+        provider_manager = ProvidersManager()
+        provider_manager._connection_form_widgets = {}
+        provider_manager._add_widgets(
+            package_name='mock',
+            hook_class=TestHook,
+            widgets={f: BooleanField(lazy_gettext('Dummy param')) for f in expected_field_names_order},
+        )
+        actual_field_names_order = tuple(
+            key for key in provider_manager.connection_form_widgets.keys() if key.startswith(field_prefix)
+        )
+        assert actual_field_names_order == expected_field_names_order, "Not keeping original fields order"
+
+    def test_connection_form_widgets_fields_order_multiple_hooks(self):
+        """
+        Check that order of connection for widgets preserved by original Hooks order.
+        Even if different hooks specified field with the same connection type.
+        """
+        test_conn_type = 'test'
+        field_prefix = f'extra__{test_conn_type}__'
+        field_names_hook_1 = ("foo", "bar", "spam", "egg")
+        field_names_hook_2 = ("yyy_param", "aaa_param", "000_param")
+
+        expected_field_names_order = tuple(
+            f"{field_prefix}{f}" for f in [*field_names_hook_1, *field_names_hook_2]
+        )
+
+        class TestHook1:
+            conn_type = test_conn_type
+
+        class TestHook2:
+            conn_type = 'another'
+
+        provider_manager = ProvidersManager()
+        provider_manager._connection_form_widgets = {}
+        provider_manager._add_widgets(
+            package_name='mock',
+            hook_class=TestHook1,
+            widgets={
+                f"{field_prefix}{f}": BooleanField(lazy_gettext('Dummy param')) for f in field_names_hook_1
+            },
+        )
+        provider_manager._add_widgets(
+            package_name='another_mock',
+            hook_class=TestHook2,
+            widgets={
+                f"{field_prefix}{f}": BooleanField(lazy_gettext('Dummy param')) for f in field_names_hook_2
+            },
+        )
+        actual_field_names_order = tuple(
+            key for key in provider_manager.connection_form_widgets.keys() if key.startswith(field_prefix)
+        )
+        assert actual_field_names_order == expected_field_names_order, "Not keeping original fields order"
+
     def test_field_behaviours(self):
         provider_manager = ProvidersManager()
         connections_with_field_behaviours = list(provider_manager.field_behaviours.keys())