Repository: slackapi/python-slackclient
Pull request #408 (2019-05-04 18:04:49)

Fix some quality issues, add .deepsource.toml file
###  Summary

Hi! It was great meeting the team at the booth in PyCon this morning!

I've added a customized `.deepsource.toml` as part of the PR that you can start using directly to run DeepSource analysis. I've also fixed some issues flagged by our analyzers while I was at it 😄

To continue running analysis on every PR, just follow these simple steps:

1. **Merge this PR.**
2. **Sign up on DeepSource** and grant access to this repository [here](https://deepsource.io/signup/).
3. **Activate analysis on this repo** [here](http://deepsource.io/gh/mongodb/mongo-python-driver).

Tip: You can also take a look at the [docs](https://deepsource.io/docs/guides/quickstart.html).

Please let me know if you have any questions! :)

### Requirements (place an `x` in each `[ ]`)

* [x] I've read and understood the [Contributing Guidelines](https://github.com/slackapi/python-slackclient/blob/master/.github/contributing.md) and have done my best effort to follow them.
* [x] I've read and agree to the [Code of Conduct](https://slackhq.github.io/code-of-conduct).

CLAassistant: [![CLA assistant check](https://cla-assistant.io/pull/badge/not_signed)](https://cla-assistant.io/slackapi/python-slackclient?pullRequest=408) <br/>Thank you for your submission, we really appreciate it. Like many open source projects, we ask that you sign our [Contributor License Agreement](https://cla-assistant.io/slackapi/python-slackclient?pullRequest=408) before we can accept your contribution.<br/><sub>You have signed the CLA already but the status is still pending? Let us [recheck](https://cla-assistant.io/check/slackapi/python-slackclient?pullRequest=408) it.</sub>
codecov[bot]: # [Codecov](https://codecov.io/gh/slackapi/python-slackclient/pull/408?src=pr&el=h1) Report
> Merging [#408](https://codecov.io/gh/slackapi/python-slackclient/pull/408?src=pr&el=desc) into [master](https://codecov.io/gh/slackapi/python-slackclient/commit/ee78eedda85b384ee060e694e2060a19e5232689?src=pr&el=desc) will **increase** coverage by `0.05%`.
> The diff coverage is `100%`.

[![Impacted file tree graph](https://codecov.io/gh/slackapi/python-slackclient/pull/408/graphs/tree.svg?width=650&token=F0O7meYmtD&height=150&src=pr)](https://codecov.io/gh/slackapi/python-slackclient/pull/408?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #408      +/-   ##
==========================================
+ Coverage   56.96%   57.02%   +0.05%     
==========================================
  Files           6        6              
  Lines         732      733       +1     
  Branches       42       42              
==========================================
+ Hits          417      418       +1     
  Misses        306      306              
  Partials        9        9
```


| [Impacted Files](https://codecov.io/gh/slackapi/python-slackclient/pull/408?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [slack/web/base\_client.py](https://codecov.io/gh/slackapi/python-slackclient/pull/408/diff?src=pr&el=tree#diff-c2xhY2svd2ViL2Jhc2VfY2xpZW50LnB5) | `84.61% <100%> (+0.19%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/slackapi/python-slackclient/pull/408?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/slackapi/python-slackclient/pull/408?src=pr&el=footer). Last update [ee78eed...e0da9d9](https://codecov.io/gh/slackapi/python-slackclient/pull/408?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/.deepsource.toml b/.deepsource.toml
new file mode 100644
index 0000000..00bf68b
--- /dev/null
+++ b/.deepsource.toml
@@ -0,0 +1,20 @@
+version = 1
+
+test_patterns = [
+  'tests/*.py',
+  'tests/**/*.py'
+]
+
+exclude_patterns = [
+  'setup.py',
+  'docs-src/**/*.py',
+  'tutorial/**/*.py'
+]
+
+[[analyzers]]
+name = 'python'
+enabled = true
+runtime_version = '3.x.x'
+
+  [analyzers.meta]
+  max_line_length = 125
diff --git a/README.md b/README.md
index 53084b6..cc30a9e 100755
--- a/README.md
+++ b/README.md
@@ -143,7 +143,7 @@ In our example below, we watch for a [message event][message-event] that contain
             )
 
     slack_token = os.environ["SLACK_API_TOKEN"]
-    rtm_client = slack.RTMClient(slack_token)
+    rtm_client = slack.RTMClient(token=slack_token)
     rtm_client.start()
 ```
 
diff --git a/documentation_v2/basic_usage.md b/documentation_v2/basic_usage.md
index 9a4c256..d6ebc1c 100755
--- a/documentation_v2/basic_usage.md
+++ b/documentation_v2/basic_usage.md
@@ -310,7 +310,7 @@ elif response["ok"] is False and response["headers"]["Retry-After"]:
 ```
 See the documentation on [Rate Limiting][rate-limiting] for more info.
 
-[auth]: basic_usage.md
+[auth]: auth.md
 [chat.postMessage]: https://api.slack.com/methods/chat.postMessage
 [chat.postEphemeral]: https://api.slack.com/methods/chat.postEphemeral
 [blockkit-builder]: https://api.slack.com/tools/block-kit-builder
@@ -325,4 +325,4 @@ See the documentation on [Rate Limiting][rate-limiting] for more info.
 [channels.leave]: https://api.slack.com/methods/channels.leave
 [users.list]: https://api.slack.com/methods/users.list
 [files.upload]: https://api.slack.com/methods/files.upload
-[rate-limiting]: https://api.slack.com/docs/rate-limits
\ No newline at end of file
+[rate-limiting]: https://api.slack.com/docs/rate-limits
diff --git a/documentation_v2/examples/asyncio.md b/documentation_v2/examples/asyncio.md
new file mode 100644
index 0000000..b44f9e5
--- /dev/null
+++ b/documentation_v2/examples/asyncio.md
@@ -0,0 +1,55 @@
+# Usage with asyncio
+
+Here is a simple example showing how to use the new `WebClient` in an asynchronous way.
+
+To run this example you will need to prepare your `SLACK_TOKEN`, ideally a Bot token so you do not have to deal with permissions scopes.
+Id of a slack channel (`MEMBERS_CHANNEL`) with users who should receive this message. You can simply obdatin this by using a browser slack client and copy the ID from a url, eg. `https://<your-workspace>.slack.com/messages/<channel ID>/`
+
+```python
+import asyncio
+import os
+
+import slack
+
+SLACK_TOKEN = <your slack token>
+MEMBERS_CHANNEL = <channel ID>
+
+# instantiate WebClient in async mode
+sc = slack.WebClient(SLACK_TOKEN, run_async=True)
+
+
+async def send_message(user_id, message):
+    """Send a direct message to the slack user."""
+    response = await sc.conversations_open(users=[user_id])
+    msg_post_response = await sc.chat_postMessage(
+        channel=response["channel"]["id"], text=message
+    )
+    return user_id, msg_post_response
+
+
+async def send_pm_to_channel_members(channel_id):
+    """Send a direct message to all users of specific channel."""
+    # get all members of the channel
+    members_response = await sc.conversations_members(channel=channel_id)
+    special_message = (
+        f"This message is supposed to be sent to users: {members_response['members']}"
+    )
+    # create coroutines and gather the responses
+    responses = await asyncio.gather(
+        *(
+            send_message(user_id, special_message)
+            for user_id in members_response["members"]
+        )
+    )
+    return responses
+
+
+def main():
+    # run the async main function
+    result = asyncio.run(send_pm_to_channel_members(MEMBERS_CHANNEL))
+    print(result)
+
+
+if __name__ == "__main__":
+    main()
+```
diff --git a/setup.py b/setup.py
index fba165e..bcff0d6 100644
--- a/setup.py
+++ b/setup.py
@@ -125,7 +125,8 @@ setup(
     packages=find_packages(
         exclude=["docs", "docs-src", "tests", "tests.*", "tutorial"]
     ),
-    install_requires=["aiohttp>3.5.2", "aiodns>1.0"],
+    install_requires=["aiohttp>3.5.2"],
+    extras_require={"optional": ["aiodns>1.0"]},
     setup_requires=["pytest-runner"],
     test_suite="tests",
     tests_require=tests_require,
diff --git a/slack/rtm/client.py b/slack/rtm/client.py
index 13d66a4..e6d462c 100644
--- a/slack/rtm/client.py
+++ b/slack/rtm/client.py
@@ -88,7 +88,7 @@ class RTMClient(object):
             )
 
     slack_token = os.environ["SLACK_API_TOKEN"]
-    rtm_client = slack.RTMClient(slack_token)
+    rtm_client = slack.RTMClient(token=slack_token)
     rtm_client.start()
     ```
 
diff --git a/slack/version.py b/slack/version.py
index 0c4721d..a52b28b 100644
--- a/slack/version.py
+++ b/slack/version.py
@@ -1,2 +1,2 @@
 # see: http://legacy.python.org/dev/peps/pep-0440/#public-version-identifiers
-__version__ = "2.0.0"
+__version__ = "2.0.1"
diff --git a/slack/web/base_client.py b/slack/web/base_client.py
index 8ac7c64..02049a4 100644
--- a/slack/web/base_client.py
+++ b/slack/web/base_client.py
@@ -102,7 +102,8 @@ class BaseClient:
             form_data = aiohttp.FormData()
             for k, v in files.items():
                 if isinstance(v, str):
-                    form_data.add_field(k, open(v, "rb"))
+                    with open(v, "rb") as fd:
+                        form_data.add_field(k, fd)
                 else:
                     form_data.add_field(k, v)
 
@@ -219,10 +220,10 @@ class BaseClient:
 
         Returns:
             The user agent string.
-            e.g. 'Python/3.6.7 slack/2.0.0 Darwin/17.7.0'
+            e.g. 'Python/3.6.7 slackclient/2.0.0 Darwin/17.7.0'
         """
         # __name__ returns all classes, we only want the client
-        client = "{0}/{1}".format(__name__.split(".")[0], ver.__version__)
+        client = "{0}/{1}".format("slackclient", ver.__version__)
         python_version = "Python/{v.major}.{v.minor}.{v.micro}".format(
             v=sys.version_info
         )
diff --git a/slack/web/client.py b/slack/web/client.py
index 90c4396..7ccd7b2 100644
--- a/slack/web/client.py
+++ b/slack/web/client.py
@@ -130,14 +130,14 @@ class WebClient(BaseClient):
         kwargs.update({"channel": channel, "user": user})
         return self.api_call("channels.invite", json=kwargs)
 
-    def channels_join(self, *, channel: str, **kwargs) -> SlackResponse:
+    def channels_join(self, *, name: str, **kwargs) -> SlackResponse:
         """Joins a channel, creating it if needed.
 
         Args:
             name (str): The channel name. e.g. '#general'
         """
         self._validate_xoxp_token()
-        kwargs.update({"channel": channel})
+        kwargs.update({"name": name})
         return self.api_call("channels.join", json=kwargs)
 
     def channels_kick(self, *, channel: str, user: str, **kwargs) -> SlackResponse:
@@ -186,7 +186,9 @@ class WebClient(BaseClient):
         kwargs.update({"channel": channel, "name": name})
         return self.api_call("channels.rename", json=kwargs)
 
-    def channels_replies(self, *, channel: str, thread_ts: str, **kwargs) -> SlackResponse:
+    def channels_replies(
+        self, *, channel: str, thread_ts: str, **kwargs
+    ) -> SlackResponse:
         """Retrieve a thread of messages posted to a channel
 
         Args:
@@ -239,6 +241,20 @@ class WebClient(BaseClient):
         kwargs.update({"channel": channel, "ts": ts})
         return self.api_call("chat.delete", json=kwargs)
 
+    def chat_deleteScheduledMessage(
+        self, *, channel: str, scheduled_message_id: str, **kwargs
+    ) -> SlackResponse:
+        """Deletes a scheduled message.
+
+        Args:
+            channel (str): The channel the scheduled_message is posting to. e.g. 'C1234567890'
+            scheduled_message_id (str): scheduled_message_id returned from call to chat.scheduleMessage e.g. 'Q1234ABCD'
+        """
+        kwargs.update(
+            {"channel": channel, "scheduled_message_id": scheduled_message_id}
+        )
+        return self.api_call("chat.deleteScheduledMessage", json=kwargs)
+
     def chat_getPermalink(
         self, *, channel: str, message_ts: str, **kwargs
     ) -> SlackResponse:
@@ -290,6 +306,19 @@ class WebClient(BaseClient):
         kwargs.update({"channel": channel})
         return self.api_call("chat.postMessage", json=kwargs)
 
+    def chat_scheduleMessage(
+        self, *, channel: str, post_at: str, text: str, **kwargs
+    ) -> SlackResponse:
+        """Schedules a message.
+
+        Args:
+            channel (str): The channel the scheduled_message is posting to. e.g. 'C1234567890'
+            post_at (str): Unix EPOCH timestamp of time in future to send the message. e.g. '299876400'
+            text (str): The message you'd like to send. e.g. 'Hello world'
+        """
+        kwargs.update({"channel": channel, "post_at": post_at, "text": text})
+        return self.api_call("chat.scheduleMessage", json=kwargs)
+
     def chat_unfurl(
         self, *, channel: str, ts: str, unfurls: dict, **kwargs
     ) -> SlackResponse:
@@ -320,6 +349,10 @@ class WebClient(BaseClient):
         kwargs.update({"channel": channel, "ts": ts})
         return self.api_call("chat.update", json=kwargs)
 
+    def chat_scheduledMessages_list(self, **kwargs) -> SlackResponse:
+        """Lists all scheduled messages."""
+        return self.api_call("chat.scheduledMessages.list", json=kwargs)
+
     def conversations_archive(self, *, channel: str, **kwargs) -> SlackResponse:
         """Archives a conversation.
 
@@ -428,7 +461,9 @@ class WebClient(BaseClient):
         """Opens or resumes a direct message or multi-person direct message."""
         return self.api_call("conversations.open", json=kwargs)
 
-    def conversations_rename(self, *, channel: str, name: str, **kwargs) -> SlackResponse:
+    def conversations_rename(
+        self, *, channel: str, name: str, **kwargs
+    ) -> SlackResponse:
         """Renames a conversation.
 
         Args:
@@ -439,7 +474,9 @@ class WebClient(BaseClient):
         kwargs.update({"channel": channel, "name": name})
         return self.api_call("conversations.rename", json=kwargs)
 
-    def conversations_replies(self, *, channel: str, ts: str, **kwargs) -> SlackResponse:
+    def conversations_replies(
+        self, *, channel: str, ts: str, **kwargs
+    ) -> SlackResponse:
         """Retrieve a thread of messages posted to a conversation
 
         Args:
@@ -544,17 +581,6 @@ class WebClient(BaseClient):
         """Lists custom emoji for a team."""
         return self.api_call("emoji.list", http_verb="GET", params=kwargs)
 
-    def files_comments_add(self, *, comment: str, file: str, **kwargs) -> SlackResponse:
-        """Add a comment to an existing file.
-
-        Args:
-            comment (str): The body of the comment.
-                e.g. 'Everyone should take a moment to read this file.'
-            file (str): The file id. e.g. 'F1234467890'
-        """
-        kwargs.update({"comment": comment, "file": file})
-        return self.api_call("files.comments.add", json=kwargs)
-
     def files_comments_delete(self, *, file: str, id: str, **kwargs) -> SlackResponse:
         """Deletes an existing comment on a file.
 
@@ -565,36 +591,22 @@ class WebClient(BaseClient):
         kwargs.update({"file": file, "id": id})
         return self.api_call("files.comments.delete", json=kwargs)
 
-    def files_comments_edit(
-        self, *, comment: str, file: str, id: str, **kwargs
-    ) -> SlackResponse:
-        """Edit an existing file comment.
-
-        Args:
-            comment (str): The body of the comment.
-                e.g. 'Everyone should take a moment to read this file.'
-            file (str): The file id. e.g. 'F1234467890'
-            id (str): The file comment id. e.g. 'Fc1234567890'
-        """
-        kwargs.update({"comment": comment, "file": file, "id": id})
-        return self.api_call("files.comments.edit", json=kwargs)
-
-    def files_delete(self, *, id: str, **kwargs) -> SlackResponse:
+    def files_delete(self, *, file: str, **kwargs) -> SlackResponse:
         """Deletes a file.
 
         Args:
-            id (str): The file id. e.g. 'F1234467890'
+            file (str): The file id. e.g. 'F1234467890'
         """
-        kwargs.update({"id": id})
+        kwargs.update({"file": file})
         return self.api_call("files.delete", json=kwargs)
 
-    def files_info(self, *, id: str, **kwargs) -> SlackResponse:
+    def files_info(self, *, file: str, **kwargs) -> SlackResponse:
         """Gets information about a team file.
 
         Args:
-            id (str): The file id. e.g. 'F1234467890'
+            file (str): The file id. e.g. 'F1234467890'
         """
-        kwargs.update({"id": id})
+        kwargs.update({"file": file})
         return self.api_call("files.info", http_verb="GET", params=kwargs)
 
     def files_list(self, **kwargs) -> SlackResponse:
@@ -602,24 +614,24 @@ class WebClient(BaseClient):
         self._validate_xoxp_token()
         return self.api_call("files.list", http_verb="GET", params=kwargs)
 
-    def files_revokePublicURL(self, *, id: str, **kwargs) -> SlackResponse:
+    def files_revokePublicURL(self, *, file: str, **kwargs) -> SlackResponse:
         """Revokes public/external sharing access for a file
 
         Args:
-            id (str): The file id. e.g. 'F1234467890'
+            file (str): The file id. e.g. 'F1234467890'
         """
         self._validate_xoxp_token()
-        kwargs.update({"id": id})
+        kwargs.update({"file": file})
         return self.api_call("files.revokePublicURL", json=kwargs)
 
-    def files_sharedPublicURL(self, *, id: str, **kwargs) -> SlackResponse:
+    def files_sharedPublicURL(self, *, file: str, **kwargs) -> SlackResponse:
         """Enables a file for public/external sharing.
 
         Args:
-            id (str): The file id. e.g. 'F1234467890'
+            file (str): The file id. e.g. 'F1234467890'
         """
         self._validate_xoxp_token()
-        kwargs.update({"id": id})
+        kwargs.update({"file": file})
         return self.api_call("files.sharedPublicURL", json=kwargs)
 
     def files_upload(
@@ -763,7 +775,9 @@ class WebClient(BaseClient):
         kwargs.update({"channel": channel, "name": name})
         return self.api_call("groups.rename", json=kwargs)
 
-    def groups_replies(self, *, channel: str, thread_ts: str, **kwargs) -> SlackResponse:
+    def groups_replies(
+        self, *, channel: str, thread_ts: str, **kwargs
+    ) -> SlackResponse:
         """Retrieve a thread of messages posted to a private channel
 
         Args:
@@ -775,7 +789,9 @@ class WebClient(BaseClient):
         kwargs.update({"channel": channel, "thread_ts": thread_ts})
         return self.api_call("groups.replies", http_verb="GET", params=kwargs)
 
-    def groups_setPurpose(self, *, channel: str, purpose: str, **kwargs) -> SlackResponse:
+    def groups_setPurpose(
+        self, *, channel: str, purpose: str, **kwargs
+    ) -> SlackResponse:
         """Sets the purpose for a private channel.
 
         Args:


Test patch:
diff --git a/tests/web/test_web_client.py b/tests/web/test_web_client.py
index 6bf450b..2907bae 100644
--- a/tests/web/test_web_client.py
+++ b/tests/web/test_web_client.py
@@ -2,6 +2,7 @@
 import unittest
 from unittest import mock
 import asyncio
+import re
 
 
 # Internal Imports
@@ -18,11 +19,34 @@ class TestWebClient(unittest.TestCase):
     def tearDown(self):
         pass
 
+    pattern_for_language = re.compile("python/(\\S+)", re.IGNORECASE)
+    pattern_for_package_identifier = re.compile("slackclient/(\\S+)")
+
     def test_api_calls_return_a_response_when_run_in_sync_mode(self, mock_request):
         resp = self.client.api_test()
         self.assertFalse(asyncio.isfuture(resp))
         self.assertTrue(resp["ok"])
 
+    def test_api_calls_include_user_agent(self, mock_request):
+        self.client.api_test()
+        mock_call_kwargs = mock_request.call_args[1]
+        self.assertIn("req_args", mock_call_kwargs)
+        mock_call_req_args = mock_call_kwargs["req_args"]
+        self.assertIn("headers", mock_call_req_args)
+        mock_call_headers = mock_call_req_args["headers"]
+        self.assertIn("User-Agent", mock_call_headers)
+        mock_call_user_agent = mock_call_headers["User-Agent"]
+        self.assertRegex(
+            mock_call_user_agent,
+            self.pattern_for_package_identifier,
+            "User Agent contains slackclient and version",
+        )
+        self.assertRegex(
+            mock_call_user_agent,
+            self.pattern_for_language,
+            "User Agent contains Python and version",
+        )
+
     @async_test
     async def test_api_calls_return_a_future_when_run_in_async_mode(self, mock_request):
         self.client.run_async = True