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 fd27584b3d Refactor `SlackWebhookOperator`: Get rid of mandatory 
http-provider dependency (#26648)
fd27584b3d is described below

commit fd27584b3dc355eaf0c0cd7a4cd65e0e580fcf6d
Author: Andrey Anshin <[email protected]>
AuthorDate: Tue Sep 27 18:41:20 2022 +0400

    Refactor `SlackWebhookOperator`: Get rid of mandatory http-provider 
dependency (#26648)
    
    * Refactor `SlackWebhookOperator`: Get rid of mandatory http-provider 
dependency
    
    * Fix tests selective tests for slack
---
 airflow/providers/slack/operators/slack_webhook.py |  89 +++++++++--
 airflow/providers/slack/provider.yaml              |   1 -
 dev/breeze/tests/test_selective_checks.py          |   3 +-
 generated/provider_dependencies.json               |   4 +-
 .../slack/operators/test_slack_webhook.py          | 169 ++++++++++++++++-----
 5 files changed, 204 insertions(+), 62 deletions(-)

diff --git a/airflow/providers/slack/operators/slack_webhook.py 
b/airflow/providers/slack/operators/slack_webhook.py
index 6872772a69..0f4c80c164 100644
--- a/airflow/providers/slack/operators/slack_webhook.py
+++ b/airflow/providers/slack/operators/slack_webhook.py
@@ -17,29 +17,40 @@
 # under the License.
 from __future__ import annotations
 
+import warnings
 from typing import TYPE_CHECKING, Sequence
 
 from airflow.compat.functools import cached_property
-from airflow.providers.http.operators.http import SimpleHttpOperator
+from airflow.exceptions import AirflowException
+from airflow.models import BaseOperator
 from airflow.providers.slack.hooks.slack_webhook import SlackWebhookHook
 
 if TYPE_CHECKING:
     from airflow.utils.context import Context
 
 
-class SlackWebhookOperator(SimpleHttpOperator):
+class SlackWebhookOperator(BaseOperator):
     """
-    This operator allows you to post messages to Slack using incoming webhooks.
-    Takes both Slack webhook token directly and connection that has Slack 
webhook token.
-    If both supplied, http_conn_id will be used as base_url,
-    and webhook_token will be taken as endpoint, the relative path of the url.
+    This operator allows you to post messages to Slack using Incoming Webhooks.
 
-    Each Slack webhook token can be pre-configured to use a specific channel, 
username and
-    icon. You can override these defaults in this hook.
+    .. note::
+        You cannot override the default channel (chosen by the user who 
installed your app),
+        username, or icon when you're using Incoming Webhooks to post messages.
+        Instead, these values will always inherit from the associated Slack 
App configuration
+        (`link 
<https://api.slack.com/messaging/webhooks#advanced_message_formatting>`_).
+        It is possible to change this values only in `Legacy Slack Integration 
Incoming Webhook
+        
<https://api.slack.com/legacy/custom-integrations/messaging/webhooks#legacy-customizations>`_.
 
-    :param http_conn_id: connection that has Slack webhook token in the extra 
field
-    :param webhook_token: Slack webhook token
-    :param message: The message you want to send on Slack
+    .. warning::
+        This operator could take Slack Webhook Token from ``webhook_token``
+        as well as from :ref:`Slack Incoming Webhook connection 
<howto/connection:slack-incoming-webhook>`.
+        However, provide ``webhook_token`` it is not secure and this attribute
+        will be removed in the future version of provider.
+
+    :param slack_webhook_conn_id: :ref:`Slack Incoming Webhook 
<howto/connection:slack>`
+        connection id that has Incoming Webhook token in the password field.
+    :param message: The formatted text of the message to be published.
+        If ``blocks`` are included, this will become the fallback text used in 
notifications.
     :param attachments: The attachments to send on Slack. Should be a list of
         dictionaries representing Slack attachments.
     :param blocks: The blocks to send on Slack. Should be a list of
@@ -51,6 +62,8 @@ class SlackWebhookOperator(SimpleHttpOperator):
     :param link_names: Whether or not to find and link channel and usernames 
in your
         message
     :param proxy: Proxy to use to make the Slack webhook call
+    :param webhook_token: (deprecated) Slack Incoming Webhook token.
+        Please use ``slack_webhook_conn_id`` instead.
     """
 
     template_fields: Sequence[str] = (
@@ -66,7 +79,7 @@ class SlackWebhookOperator(SimpleHttpOperator):
     def __init__(
         self,
         *,
-        http_conn_id: str,
+        slack_webhook_conn_id: str | None = None,
         webhook_token: str | None = None,
         message: str = "",
         attachments: list | None = None,
@@ -79,8 +92,50 @@ class SlackWebhookOperator(SimpleHttpOperator):
         proxy: str | None = None,
         **kwargs,
     ) -> None:
-        super().__init__(endpoint=webhook_token, **kwargs)
-        self.http_conn_id = http_conn_id
+        http_conn_id = kwargs.pop("http_conn_id", None)
+        if http_conn_id:
+            warnings.warn(
+                'Parameter `http_conn_id` is deprecated. Please use 
`slack_webhook_conn_id` instead.',
+                DeprecationWarning,
+                stacklevel=2,
+            )
+            if slack_webhook_conn_id:
+                raise AirflowException("You cannot provide both 
`slack_webhook_conn_id` and `http_conn_id`.")
+            slack_webhook_conn_id = http_conn_id
+
+        # Compatibility with previous version of operator which based on 
SimpleHttpOperator.
+        # Users might pass these arguments previously, however its never pass 
to SlackWebhookHook.
+        # We remove this arguments if found in ``kwargs`` and notify users if 
found any.
+        deprecated_class_attrs = []
+        for deprecated_attr in (
+            "endpoint",
+            "method",
+            "data",
+            "headers",
+            "response_check",
+            "response_filter",
+            "extra_options",
+            "log_response",
+            "auth_type",
+            "tcp_keep_alive",
+            "tcp_keep_alive_idle",
+            "tcp_keep_alive_count",
+            "tcp_keep_alive_interval",
+        ):
+            if deprecated_attr in kwargs:
+                deprecated_class_attrs.append(deprecated_attr)
+                kwargs.pop(deprecated_attr)
+        if deprecated_class_attrs:
+            warnings.warn(
+                f"Provide {','.join(repr(a) for a in deprecated_class_attrs)} 
is deprecated "
+                f"and as has no affect, please remove it from 
{self.__class__.__name__} "
+                "constructor attributes otherwise in future version of 
provider it might cause an issue.",
+                DeprecationWarning,
+                stacklevel=2,
+            )
+
+        super().__init__(**kwargs)
+        self.slack_webhook_conn_id = slack_webhook_conn_id
         self.webhook_token = webhook_token
         self.proxy = proxy
         self.message = message
@@ -94,10 +149,12 @@ class SlackWebhookOperator(SimpleHttpOperator):
 
     @cached_property
     def hook(self) -> SlackWebhookHook:
+        """Create and return an SlackWebhookHook (cached)."""
         return SlackWebhookHook(
-            http_conn_id=self.http_conn_id,
-            webhook_token=self.webhook_token,
+            slack_webhook_conn_id=self.slack_webhook_conn_id,
             proxy=self.proxy,
+            # Deprecated. SlackWebhookHook will notify user if user provide 
non-empty ``webhook_token``.
+            webhook_token=self.webhook_token,
         )
 
     def execute(self, context: Context) -> None:
diff --git a/airflow/providers/slack/provider.yaml 
b/airflow/providers/slack/provider.yaml
index a5cadde587..3b60cb19d3 100644
--- a/airflow/providers/slack/provider.yaml
+++ b/airflow/providers/slack/provider.yaml
@@ -38,7 +38,6 @@ versions:
 dependencies:
   - apache-airflow>=2.2.0
   - apache-airflow-providers-common-sql>=1.1.0
-  - apache-airflow-providers-http
   - slack_sdk>=3.0.0
 
 integrations:
diff --git a/dev/breeze/tests/test_selective_checks.py 
b/dev/breeze/tests/test_selective_checks.py
index 4d81aede2d..264e91b928 100644
--- a/dev/breeze/tests/test_selective_checks.py
+++ b/dev/breeze/tests/test_selective_checks.py
@@ -153,8 +153,7 @@ def assert_outputs_are_printed(expected_outputs: dict[str, 
str], output: str):
                     "docs-build": "true",
                     "run-kubernetes-tests": "true",
                     "upgrade-to-newer-dependencies": "false",
-                    "test-types": "Always Providers[airbyte,apache.livy,"
-                    "dbt.cloud,dingding,discord,http,slack]",
+                    "test-types": "Always 
Providers[airbyte,apache.livy,dbt.cloud,dingding,discord,http]",
                 },
                 id="Helm tests, http and all relevant providers, kubernetes 
tests and "
                 "docs should run even if unimportant files were added",
diff --git a/generated/provider_dependencies.json 
b/generated/provider_dependencies.json
index 44b5873d22..12ead6e874 100644
--- a/generated/provider_dependencies.json
+++ b/generated/provider_dependencies.json
@@ -654,13 +654,11 @@
   "slack": {
     "deps": [
       "apache-airflow-providers-common-sql>=1.1.0",
-      "apache-airflow-providers-http",
       "apache-airflow>=2.2.0",
       "slack_sdk>=3.0.0"
     ],
     "cross-providers-deps": [
-      "common.sql",
-      "http"
+      "common.sql"
     ]
   },
   "snowflake": {
diff --git a/tests/providers/slack/operators/test_slack_webhook.py 
b/tests/providers/slack/operators/test_slack_webhook.py
index 6f07e86357..f35d08a285 100644
--- a/tests/providers/slack/operators/test_slack_webhook.py
+++ b/tests/providers/slack/operators/test_slack_webhook.py
@@ -17,55 +17,97 @@
 # under the License.
 from __future__ import annotations
 
-import unittest
-from typing import Sequence
+from unittest import mock
 
-from airflow.models.dag import DAG
+import pytest
+
+from airflow.exceptions import AirflowException
 from airflow.providers.slack.operators.slack_webhook import 
SlackWebhookOperator
-from airflow.utils import timezone
 
-DEFAULT_DATE = timezone.datetime(2017, 1, 1)
 
+class TestSlackWebhookOperator:
+    def setup_method(self):
+        self.default_op_kwargs = {
+            "channel": None,
+            "username": None,
+            "icon_emoji": None,
+            "icon_url": None,
+        }
 
-class TestSlackWebhookOperator(unittest.TestCase):
-    _config = {
-        'http_conn_id': 'slack-webhook-default',
-        'webhook_token': 'manual_token',
-        'message': 'your message here',
-        'attachments': [{'fallback': 'Required plain-text summary'}],
-        'blocks': [{'type': 'section', 'text': {'type': 'mrkdwn', 'text': 
'*bold text*'}}],
-        'channel': '#general',
-        'username': 'SlackMcSlackFace',
-        'icon_emoji': ':hankey',
-        'icon_url': 'https://airflow.apache.org/_images/pin_large.png',
-        'link_names': True,
-        'proxy': 'https://my-horrible-proxy.proxyist.com:8080',
-    }
+    @pytest.mark.parametrize(
+        "simple_http_op_attr",
+        [
+            "endpoint",
+            "method",
+            "data",
+            "headers",
+            "response_check",
+            "response_filter",
+            "extra_options",
+            "log_response",
+            "auth_type",
+            "tcp_keep_alive",
+            "tcp_keep_alive_idle",
+            "tcp_keep_alive_count",
+            "tcp_keep_alive_interval",
+        ],
+    )
+    def test_unused_deprecated_http_operator_kwargs(self, simple_http_op_attr):
+        """
+        Test remove deprecated (and unused) SimpleHttpOperator keyword 
arguments.
+        No error should happen if provide any of attribute, unless operator 
allow to provide this attributes.
+        """
+        kw = {simple_http_op_attr: "foo-bar"}
+        warning_message = fr"Provide '{simple_http_op_attr}' is deprecated and 
as has no affect"
+        with pytest.warns(DeprecationWarning, match=warning_message):
+            SlackWebhookOperator(task_id="test_unused_args", **kw)
 
-    def setUp(self):
-        args = {'owner': 'airflow', 'start_date': DEFAULT_DATE}
-        self.dag = DAG('test_dag_id', default_args=args)
+    def test_deprecated_http_conn_id(self):
+        """Test resolve deprecated http_conn_id."""
+        warning_message = (
+            r"Parameter `http_conn_id` is deprecated. Please use 
`slack_webhook_conn_id` instead."
+        )
+        with pytest.warns(DeprecationWarning, match=warning_message):
+            op = SlackWebhookOperator(
+                task_id='test_deprecated_http_conn_id', 
slack_webhook_conn_id=None, http_conn_id="http_conn"
+            )
+        assert op.slack_webhook_conn_id == "http_conn"
 
-    def test_execute(self):
-        # Given / When
-        operator = SlackWebhookOperator(task_id='slack_webhook_job', 
dag=self.dag, **self._config)
+        error_message = "You cannot provide both `slack_webhook_conn_id` and 
`http_conn_id`."
+        with pytest.raises(AirflowException, match=error_message):
+            with pytest.warns(DeprecationWarning, match=warning_message):
+                SlackWebhookOperator(
+                    task_id='test_both_conn_ids',
+                    slack_webhook_conn_id="slack_webhook_conn_id",
+                    http_conn_id="http_conn",
+                )
 
-        assert self._config['http_conn_id'] == operator.http_conn_id
-        assert self._config['webhook_token'] == operator.webhook_token
-        assert self._config['message'] == operator.message
-        assert self._config['attachments'] == operator.attachments
-        assert self._config['blocks'] == operator.blocks
-        assert self._config['channel'] == operator.channel
-        assert self._config['username'] == operator.username
-        assert self._config['icon_emoji'] == operator.icon_emoji
-        assert self._config['icon_url'] == operator.icon_url
-        assert self._config['link_names'] == operator.link_names
-        assert self._config['proxy'] == operator.proxy
+    @pytest.mark.parametrize(
+        "slack_webhook_conn_id,webhook_token",
+        [
+            ("test_conn_id", None),
+            (None, "https://hooks.slack.com/services/T000/B000/XXX";),
+            ("test_conn_id", "https://hooks.slack.com/services/T000/B000/XXX";),
+        ],
+    )
+    @pytest.mark.parametrize("proxy", [None, "https://localhost:9999";])
+    
@mock.patch("airflow.providers.slack.operators.slack_webhook.SlackWebhookHook")
+    def test_hook(self, mock_slackwebhook_cls, slack_webhook_conn_id, 
webhook_token, proxy):
+        """Test get cached ``SlackWebhookHook`` hook."""
+        op_kw = {
+            "slack_webhook_conn_id": slack_webhook_conn_id,
+            "proxy": proxy,
+            "webhook_token": webhook_token,
+        }
+        op = SlackWebhookOperator(task_id='test_hook', **op_kw)
+        hook = op.hook
+        assert hook is op.hook, "Expected cached hook"
+        mock_slackwebhook_cls.assert_called_once_with(**op_kw)
 
     def test_assert_templated_fields(self):
-        operator = SlackWebhookOperator(task_id='slack_webhook_job', 
dag=self.dag, **self._config)
-
-        template_fields: Sequence[str] = (
+        """Test expected templated fields."""
+        operator = 
SlackWebhookOperator(task_id='test_assert_templated_fields', 
**self.default_op_kwargs)
+        template_fields = (
             'webhook_token',
             'message',
             'attachments',
@@ -74,5 +116,52 @@ class TestSlackWebhookOperator(unittest.TestCase):
             'username',
             'proxy',
         )
-
         assert operator.template_fields == template_fields
+
+    @pytest.mark.parametrize(
+        "message,blocks,attachments",
+        [
+            ("Test Text", ["Dummy Block"], ["Test Attachments"]),
+            ("Test Text", ["Dummy Block"], None),
+            ("Test Text", None, None),
+            (None, ["Dummy Block"], None),
+            (None, ["Dummy Block"], ["Test Attachments"]),
+            (None, None, ["Test Attachments"]),
+        ],
+    )
+    @pytest.mark.parametrize(
+        "channel,username,icon_emoji,icon_url",
+        [
+            (None, None, None, None),
+            ("legacy-channel", "legacy-username", "legacy-icon_emoji", 
"legacy-icon-url"),
+        ],
+        ids=["webhook-attrs", "legacy-webhook-attrs"],
+    )
+    
@mock.patch("airflow.providers.slack.operators.slack_webhook.SlackWebhookHook")
+    def test_execute_operator(
+        self, mock_slackwebhook_cls, message, blocks, attachments, channel, 
username, icon_emoji, icon_url
+    ):
+        mock_slackwebhook = mock_slackwebhook_cls.return_value
+        mock_slackwebhook_send = mock_slackwebhook.send
+        op = SlackWebhookOperator(
+            task_id="test_execute",
+            slack_webhook_conn_id="test_conn_id",
+            message=message,
+            blocks=blocks,
+            attachments=attachments,
+            channel=channel,
+            username=username,
+            icon_emoji=icon_emoji,
+            icon_url=icon_url,
+        )
+        op.execute(mock.MagicMock())
+        mock_slackwebhook_send.assert_called_once_with(
+            text=message,
+            blocks=blocks,
+            attachments=attachments,
+            channel=channel,
+            username=username,
+            icon_emoji=icon_emoji,
+            icon_url=icon_url,
+            link_names=mock.ANY,
+        )

Reply via email to