anon-iclr-submission/benchname-bug-localization huggingface dataset:
Repository: chemprop/chemprop
Issue: [BUG]: AttributeError: 'list' object has no attribute 'shape' 

If I try the scripts/tsne.py, then bug occurs
```
$ python3 scripts/tsne.py --smiles_paths tests/data/classification.csv --save_path tests/data/classification.png
```
```
Loading data
classification: 500
Computing Morgan fingerprints
  0%|                                                                                                                                                                                                                 | 0/500 [00:00<?, ?it/s][15:12:09] WARNING: not removing hydrogen atom without neighbors
100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 500/500 [00:00<00:00, 4214.68it/s]
Running t-SNE
Traceback (most recent call last):
  File "/opt/chemprop/scripts/tsne.py", line 122, in <module>
    compare_datasets_tsne(Args().parse_args())
  File "/opt/chemprop/scripts/tsne.py", line 70, in compare_datasets_tsne
    X = tsne.fit_transform(morgans)
  File "/opt/conda/lib/python3.9/site-packages/sklearn/manifold/_t_sne.py", line 1118, in fit_transform
    self._check_params_vs_input(X)
  File "/opt/conda/lib/python3.9/site-packages/sklearn/manifold/_t_sne.py", line 828, in _check_params_vs_input
    if self.perplexity >= X.shape[0]:
AttributeError: 'list' object has no attribute 'shape'
```


Changed files:
scripts/tsne.py

diff --git a/scripts/tsne.py b/scripts/tsne.py
index f206af43..97bdaf6b 100644
--- a/scripts/tsne.py
+++ b/scripts/tsne.py
@@ -62,7 +62,7 @@ def compare_datasets_tsne(args: Args):
     # Compute Morgan fingerprints
     print('Computing Morgan fingerprints')
     morgan_generator = get_features_generator('morgan')
-    morgans = [morgan_generator(smile) for smile in tqdm(smiles, total=len(smiles))]
+    morgans = np.array([morgan_generator(smile) for smile in tqdm(smiles, total=len(smiles))])
 
     print('Running t-SNE')
     start = time.time()
Repository: python-discord/bot
Issue: Make `users` argument of purge command required

When `user` argument is not provided to the `!purge` command, the greedy convertor returns an empty list. This results in the bot deleting messages of ALL users in the last 10 minutes.
Some kind of a check should be added to prevent this from happening.
Also applicable to the `!clean users` command.


Changed files:
bot/exts/moderation/clean.py

diff --git a/bot/exts/moderation/clean.py b/bot/exts/moderation/clean.py
index d7c2c767..748c018d 100644
--- a/bot/exts/moderation/clean.py
+++ b/bot/exts/moderation/clean.py
@@ -627,12 +627,18 @@ class Clean(Cog):
     @command()
     async def purge(self, ctx: Context, users: Greedy[User], age: Optional[Union[Age, ISODateTime]] = None) -> None:
         """
-        Clean messages of users from all public channels up to a certain message age (10 minutes by default).
+        Clean messages of `users` from all public channels up to a certain message `age` (10 minutes by default).
 
-        The age is *exclusive*, meaning that `10s` won't delete a message exactly 10 seconds old.
+        Requires 1 or more users to be specified. For channel-based cleaning, use `clean` instead.
+
+        `age` can be a duration or an ISO 8601 timestamp.
         """
+        if not users:
+            raise BadArgument("At least one user must be specified.")
+
         if age is None:
             age = await Age().convert(ctx, "10M")
+
         await self._clean_messages(ctx, channels="*", users=users, first_limit=age)
 
     # endregion
Repository: python-discord/bot
Issue: Async function remove_cog not awaited

If you take a look at this line
https://github.com/python-discord/bot/blob/71fbd5aa7ef8fdf3547dbae845cf7e6b9bc08277/bot/exts/moderation/watchchannels/_watchchannel.py#L131

You'll see that the remove_cog async func isn't awaited.

Changed files:
bot/exts/moderation/watchchannels/_watchchannel.py, bot/exts/filters/antispam.py, bot/exts/help_channels/_cog.py

diff --git a/bot/exts/filters/antispam.py b/bot/exts/filters/antispam.py
index 842aab38..b4e7a33f 100644
--- a/bot/exts/filters/antispam.py
+++ b/bot/exts/filters/antispam.py
@@ -156,7 +156,7 @@ class AntiSpam(Cog):
                 colour=Colour.red()
             )
 
-            self.bot.remove_cog(self.__class__.__name__)
+            await self.bot.remove_cog(self.__class__.__name__)
             return
 
     @Cog.listener()
diff --git a/bot/exts/help_channels/_cog.py b/bot/exts/help_channels/_cog.py
index f1351013..31a33f8a 100644
--- a/bot/exts/help_channels/_cog.py
+++ b/bot/exts/help_channels/_cog.py
@@ -315,7 +315,7 @@ class HelpChannels(commands.Cog):
             )
         except discord.HTTPException:
             log.exception("Failed to get a category; cog will be removed")
-            self.bot.remove_cog(self.qualified_name)
+            await self.bot.remove_cog(self.qualified_name)
 
     async def cog_load(self) -> None:
         """Initialise the help channel system."""
diff --git a/bot/exts/moderation/watchchannels/_watchchannel.py b/bot/exts/moderation/watchchannels/_watchchannel.py
index 46f9c296..6eaedf6b 100644
--- a/bot/exts/moderation/watchchannels/_watchchannel.py
+++ b/bot/exts/moderation/watchchannels/_watchchannel.py
@@ -128,7 +128,7 @@ class WatchChannel(metaclass=CogABCMeta):
                 colour=Color.red()
             )
 
-            self.bot.remove_cog(self.__class__.__name__)
+            await self.bot.remove_cog(self.__class__.__name__)
             return
 
         if not await self.fetch_user_cache():
Repository: python-discord/bot
Issue: !zen command not working as intended

if you do
```
!zen explicit
```
it returns 
```
**The Zen of Python (line 10):**
Unless explicitly silenced.
```

while I do believe that the intent was to have it return line two
```
**The Zen of Python (line 1):**
Explicit is better than implicit.
```


Changed files:
bot/cogs/utils.py

diff --git a/bot/cogs/utils.py b/bot/cogs/utils.py
index f0b1172e..3ed471bb 100644
--- a/bot/cogs/utils.py
+++ b/bot/cogs/utils.py
@@ -236,7 +236,17 @@ class Utils(Cog):
             await ctx.send(embed=embed)
             return
 
-        # handle if it's a search string
+        # Try to handle first exact word due difflib.SequenceMatched may use some other similar word instead
+        # exact word.
+        for i, line in enumerate(zen_lines):
+            for word in line.split():
+                if word.lower() == search_value.lower():
+                    embed.title += f" (line {i}):"
+                    embed.description = line
+                    await ctx.send(embed=embed)
+                    return
+
+        # handle if it's a search string and not exact word
         matcher = difflib.SequenceMatcher(None, search_value.lower())
 
         best_match = ""
Repository: python-discord/bot
Issue: AttributeError: 'NoneType' object has no attribute 'id'

Sentry Issue: [BOT-36E](https://sentry.io/organizations/python-discord/issues/3401907515/?referrer=github_integration)

```
AttributeError: 'NoneType' object has no attribute 'id'
  File "discord/client.py", line 456, in _run_event
    await coro(*args, **kwargs)
  File "bot/exts/moderation/modlog.py", line 673, in on_raw_message_delete
    await self.log_uncached_deleted_message(event)
  File "bot/exts/moderation/modlog.py", line 634, in log_uncached_deleted_message
    if self.is_channel_ignored(event.channel_id):
  File "bot/exts/moderation/modlog.py", line 555, in is_channel_ignored
    if not channel or not hasattr(channel, "guild") or channel.guild.id != GuildConstant.id:

Unhandled exception in on_raw_message_delete.
```

Changed files:
bot/exts/moderation/modlog.py

diff --git a/bot/exts/moderation/modlog.py b/bot/exts/moderation/modlog.py
index 67991730..efa87ce2 100644
--- a/bot/exts/moderation/modlog.py
+++ b/bot/exts/moderation/modlog.py
@@ -552,7 +552,7 @@ class ModLog(Cog, name="ModLog"):
         channel = self.bot.get_channel(channel_id)
 
         # Ignore not found channels, DMs, and messages outside of the main guild.
-        if not channel or not hasattr(channel, "guild") or channel.guild.id != GuildConstant.id:
+        if not channel or channel.guild is None or channel.guild.id != GuildConstant.id:
             return True
 
         # Look at the parent channel of a thread.
Repository: python-discord/bot
Issue: Unnecessary error logging when cleanbanning a banned user

Sentry Issue: [BOT-2ZT](https://sentry.io/organizations/python-discord/issues/3092995113/?referrer=github_integration)

```
Failed to apply ban to user xxxxxxxxxxxxx
```

See https://github.com/python-discord/bot/blob/f9fff6eaae409c7ba12ea6f95873aa0fd0340554/bot/exts/moderation/infraction/infractions.py#L125

I think this happens when a user is cleanbanned after being banned. We can probably just remove this `log.error` as `apply_ban` already does it's own logging.

*Opened by wookie184*

Changed files:
bot/exts/moderation/infraction/infractions.py

diff --git a/bot/exts/moderation/infraction/infractions.py b/bot/exts/moderation/infraction/infractions.py
index 88129840..60b4428b 100644
--- a/bot/exts/moderation/infraction/infractions.py
+++ b/bot/exts/moderation/infraction/infractions.py
@@ -126,8 +126,6 @@ class Infractions(InfractionScheduler, commands.Cog):
         infraction = await self.apply_ban(ctx, user, reason, duration_or_expiry=duration)
         if not infraction or not infraction.get("id"):
             # Ban was unsuccessful, quit early.
-            await ctx.send(":x: Failed to apply ban.")
-            log.error("Failed to apply ban to user %d", user.id)
             return
 
         # Calling commands directly skips discord.py's convertors, so we need to convert args manually.
Repository: python-discord/bot
Issue: !clean command returns malformed API request

Since we added attachments to the deleted message logs the clean command was not updated to store this.

When running a clean command the following error is returned:
```
web_1       | Bad Request: /bot/deleted-messages
web_1       | "POST /bot/deleted-messages HTTP/1.1" 400 792
bot_1       | 2020-02-25 19:32:41,081 | bot.cogs.error_handler | DEBUG | API responded with 400 for command clean all: {'deletedmessage_set': [{'attachments': ['This field may not be null.']}, {'attachments': ['This field may not be null.']}, {'attachments': ['This field may not be null.']}, {'attachments': ['This field may not be null.']}, {'attachments': ['This field may not be null.']}, {'attachments': ['This field may not be null.']}, {'attachments': ['This field may not be null.']}, {'attachments': ['This field may not be null.']}, {'attachments': ['This field may not be null.']}, {'attachments': ['This field may not be null.']}, {'attachments': ['This field may not be null.']}, {'attachments': ['This field may not be null.']}, {'attachments': ['This field may not be null.']}, {'attachments': ['This field may not be null.']}, {'attachments': ['This field may not be null.']}, {'attachments': ['This field may not be null.']}]}.
```

This is not critical since the clean command still operates, it just does not store deleted messages like intended.

Changed files:
bot/cogs/moderation/modlog.py

diff --git a/bot/cogs/moderation/modlog.py b/bot/cogs/moderation/modlog.py
index 59ae6b58..81d95298 100644
--- a/bot/cogs/moderation/modlog.py
+++ b/bot/cogs/moderation/modlog.py
@@ -67,7 +67,7 @@ class ModLog(Cog, name="ModLog"):
                         'embeds': [embed.to_dict() for embed in message.embeds],
                         'attachments': attachment,
                     }
-                    for message, attachment in zip_longest(messages, attachments)
+                    for message, attachment in zip_longest(messages, attachments, fillvalue=[])
                 ]
             }
         )
Repository: python-discord/bot
Issue: Syncer not catching asyncio.TimeoutError

It's catching the other `TimeoutError`. Simple fix is to change the exception being caught.

https://github.com/python-discord/bot/blob/7571cabe65e39d231523e713923cd23b927225bc/bot/cogs/sync/syncers.py#L125-L127

Changed files:
tests/bot/cogs/sync/test_base.py, bot/cogs/sync/syncers.py

diff --git a/bot/cogs/sync/syncers.py b/bot/cogs/sync/syncers.py
index 003bf372..e55bf27f 100644
--- a/bot/cogs/sync/syncers.py
+++ b/bot/cogs/sync/syncers.py
@@ -1,4 +1,5 @@
 import abc
+import asyncio
 import logging
 import typing as t
 from collections import namedtuple
@@ -122,7 +123,7 @@ class Syncer(abc.ABC):
                 check=partial(self._reaction_check, author, message),
                 timeout=constants.Sync.confirm_timeout
             )
-        except TimeoutError:
+        except asyncio.TimeoutError:
             # reaction will remain none thus sync will be aborted in the finally block below.
             log.debug(f"The {self.name} syncer confirmation prompt timed out.")
 
diff --git a/tests/bot/cogs/sync/test_base.py b/tests/bot/cogs/sync/test_base.py
index 6ee9dfda..70aea2ba 100644
--- a/tests/bot/cogs/sync/test_base.py
+++ b/tests/bot/cogs/sync/test_base.py
@@ -1,3 +1,4 @@
+import asyncio
 import unittest
 from unittest import mock
 
@@ -211,7 +212,7 @@ class SyncerConfirmationTests(unittest.IsolatedAsyncioTestCase):
         subtests = (
             (constants.Emojis.check_mark, True, None),
             ("InVaLiD", False, None),
-            (None, False, TimeoutError),
+            (None, False, asyncio.TimeoutError),
         )
 
         for emoji, ret_val, side_effect in subtests:
Repository: python-discord/bot
Issue: The off-topic channel name updating task fails on non-success API response

The background task we create to update off-topic names at midnight UTC fails if it receives a non-success API response. The reason is that our `bot.api_client` will raise the `bot.api.ResponseCodeError` exception on non-success response status codes. This means that the off-topic channel names won't be updated again until either the bot is restarted or the task is started manually again by an admin.

The relevant lines of code:
https://github.com/python-discord/bot/blob/e70c96248bd7b548412811a4f1ffe88bed41f815/bot/cogs/off_topic_names.py#L59-L61

To handle it, we could simply include a `try-except` block and log the exception in the `except` block. I'm not sure if we want to log the entire exception, since the exception text could be a [massive HTML-response generated by cloudflare](https://paste.pythondiscord.com/ohibicedif). Logging the failure with the response code should generally give us enough to determine the cause of the failure.

Changed files:
bot/cogs/off_topic_names.py

diff --git a/bot/cogs/off_topic_names.py b/bot/cogs/off_topic_names.py
index 8f1af347..16717d52 100644
--- a/bot/cogs/off_topic_names.py
+++ b/bot/cogs/off_topic_names.py
@@ -6,6 +6,7 @@ from datetime import datetime, timedelta
 from discord import Colour, Embed
 from discord.ext.commands import BadArgument, Bot, Cog, Context, Converter, group
 
+from bot.api import ResponseCodeError
 from bot.constants import Channels, MODERATION_ROLES
 from bot.decorators import with_role
 from bot.pagination import LinePaginator
@@ -49,9 +50,13 @@ async def update_names(bot: Bot) -> None:
         seconds_to_sleep = (next_midnight - datetime.utcnow()).seconds + 1
         await asyncio.sleep(seconds_to_sleep)
 
-        channel_0_name, channel_1_name, channel_2_name = await bot.api_client.get(
-            'bot/off-topic-channel-names', params={'random_items': 3}
-        )
+        try:
+            channel_0_name, channel_1_name, channel_2_name = await bot.api_client.get(
+                'bot/off-topic-channel-names', params={'random_items': 3}
+            )
+        except ResponseCodeError as e:
+            log.error(f"Failed to get new off topic channel names: code {e.response.status}")
+            continue
         channel_0, channel_1, channel_2 = (bot.get_channel(channel_id) for channel_id in CHANNELS)
 
         await channel_0.edit(name=f'ot0-{channel_0_name}')
Repository: python-discord/bot
Issue: Infraction scheduler raises exception when marking infraction as "permanent" during edit

When an existing temporary infraction is turned into a permanent infraction using the `!infraction edit` functionality, we ask the infraction scheduler to schedule a new task with `infraction["expires_at"] = None`. However, the scheduler expects to be called only for infractions that have a datetime string mapped to the `"expires_at"` key. This means that the scheduler will try to parse that `None` value with `dateutil.parse.isoparse`, which results in an exception:

```py
Traceback (most recent call last):
  File "/bot/bot/cogs/moderation/scheduler.py", line 415, in _scheduled_task
    expiry = dateutil.parser.isoparse(infraction["expires_at"]).replace(tzinfo=None)
  File "/usr/local/lib/python3.7/site-packages/dateutil/parser/isoparser.py", line 37, in func
    return f(self, str_in, *args, **kwargs)
  File "/usr/local/lib/python3.7/site-packages/dateutil/parser/isoparser.py", line 134, in isoparse
    components, pos = self._parse_isodate(dt_str)
  File "/usr/local/lib/python3.7/site-packages/dateutil/parser/isoparser.py", line 208, in _parse_isodate
    return self._parse_isodate_common(dt_str)
  File "/usr/local/lib/python3.7/site-packages/dateutil/parser/isoparser.py", line 213, in _parse_isodate_common
    len_str = len(dt_str)
TypeError: object of type 'NoneType' has no len()
```

I think the wisest course of action is tackle this at the level of the infraction edit method, not in the scheduler itself. We should just not try to schedule a new task when we've marked it as permanent. That means this part need to get an additional check:

https://github.com/python-discord/bot/blob/95ae05c773e753699e6899255783c1d7df936024/bot/cogs/moderation/management.py#L131-L134

**Important:** We should still *cancel* the existing scheduled task (which we currently do, it happens before the exception), so skipping this `if` block entirely will break the infraction edit in a more severe way: Cancelling the task is essential to guarantee that we don't accidentally "pardon" a now permanent infraction.

Changed files:
bot/cogs/moderation/management.py

diff --git a/bot/cogs/moderation/management.py b/bot/cogs/moderation/management.py
index 0636422d..f2964cd7 100644
--- a/bot/cogs/moderation/management.py
+++ b/bot/cogs/moderation/management.py
@@ -130,8 +130,11 @@ class ModManagement(commands.Cog):
         # Re-schedule infraction if the expiration has been updated
         if 'expires_at' in request_data:
             self.infractions_cog.cancel_task(new_infraction['id'])
-            loop = asyncio.get_event_loop()
-            self.infractions_cog.schedule_task(loop, new_infraction['id'], new_infraction)
+
+            # If the infraction was not marked as permanent, schedule a new expiration task
+            if request_data['expires_at']:
+                loop = asyncio.get_event_loop()
+                self.infractions_cog.schedule_task(loop, new_infraction['id'], new_infraction)
 
             log_text += f"""
                 Previous expiry: {old_infraction['expires_at'] or "Permanent"}
Repository: python-discord/bot
Issue: Nickname Changes Not Consistently Logged to #user-log

We noticed this morning that not all user update events are being logged to the #user-log channel for a currently unknown reason.

@SebastiaanZ indicated that he was going to take a look and see if some of the recent changes affected the logging logic.

Changed files:
bot/cogs/moderation/modlog.py

diff --git a/bot/cogs/moderation/modlog.py b/bot/cogs/moderation/modlog.py
index 86eab55d..92e9b0ef 100644
--- a/bot/cogs/moderation/modlog.py
+++ b/bot/cogs/moderation/modlog.py
@@ -20,7 +20,7 @@ GUILD_CHANNEL = t.Union[discord.CategoryChannel, discord.TextChannel, discord.Vo
 
 CHANNEL_CHANGES_UNSUPPORTED = ("permissions",)
 CHANNEL_CHANGES_SUPPRESSED = ("_overwrites", "position")
-MEMBER_CHANGES_SUPPRESSED = ("status", "activities", "_client_status")
+MEMBER_CHANGES_SUPPRESSED = ("status", "activities", "_client_status", "nick")
 ROLE_CHANGES_UNSUPPORTED = ("colour", "permissions")
 
 
@@ -498,6 +498,11 @@ class ModLog(Cog, name="ModLog"):
                 f"**Discriminator:** `{before.discriminator}` **->** `{after.discriminator}`"
             )
 
+        if before.display_name != after.display_name:
+            changes.append(
+                f"**Display name:** `{before.display_name}` **->** `{after.display_name}`"
+            )
+
         if not changes:
             return
Repository: python-discord/bot
Issue: The `unload` method fails for watchchannels if the consume task hasn't started yet

The `unload_cog` method of the `WatchChannel` ABC has a small bug in it. If you try to reload or unload the cog while the message consumption task has not yet started, it will fail because the `bot._consume_task` attribute will still be assigned to `None`:

https://github.com/python-discord/bot/blob/cff9cabfee014e6207e156ee69e8b326ece55800/bot/cogs/watchchannels/watchchannel.py#L335-L339

This will lead to the following exception, which also prevents us from reloading the extensions that have cogs that derive from this ABC completely:

```py
Traceback (most recent call last):
  File "/home/sebastiaan/pydis/repositories/djangobot/bot/cogs/cogs.py", line 232, in reload_command
    self.bot.unload_extension(full_cog)
  File "/home/sebastiaan/.local/share/virtualenvs/djangobot-Py5o4YeS/lib/python3.7/site-packages/discord/ext/commands/bot.py", line 655, in unload_extension
    self._remove_module_references(lib.__name__)
  File "/home/sebastiaan/.local/share/virtualenvs/djangobot-Py5o4YeS/lib/python3.7/site-packages/discord/ext/commands/bot.py", line 534, in _remove_module_references
    self.remove_cog(cogname)
  File "/home/sebastiaan/.local/share/virtualenvs/djangobot-Py5o4YeS/lib/python3.7/site-packages/discord/ext/commands/bot.py", line 520, in remove_cog
    cog._eject(self)
  File "/home/sebastiaan/.local/share/virtualenvs/djangobot-Py5o4YeS/lib/python3.7/site-packages/discord/ext/commands/cog.py", line 414, in _eject
    self.cog_unload()
  File "/home/sebastiaan/pydis/repositories/djangobot/bot/cogs/watchchannels/watchchannel.py", line 338, in cog_unload
    if not self._consume_task.done():
AttributeError: 'NoneType' object has no attribute 'done'
```

So, we need to make sure that a `Task` was assigned in the first place, before trying to access the `done` method of it.

Changed files:
bot/cogs/watchchannels/watchchannel.py

diff --git a/bot/cogs/watchchannels/watchchannel.py b/bot/cogs/watchchannels/watchchannel.py
index ce8014d6..760e012e 100644
--- a/bot/cogs/watchchannels/watchchannel.py
+++ b/bot/cogs/watchchannels/watchchannel.py
@@ -335,7 +335,7 @@ class WatchChannel(metaclass=CogABCMeta):
     def cog_unload(self) -> None:
         """Takes care of unloading the cog and canceling the consumption task."""
         self.log.trace(f"Unloading the cog")
-        if not self._consume_task.done():
+        if self._consume_task and not self._consume_task.done():
             self._consume_task.cancel()
             try:
                 self._consume_task.result()
Repository: python-discord/bot
Issue: Tags can't be edited due to using the POST method with the API

Currently, the `!tag edit` subcommand is just an alias of `!tag set`. This means that if we try to edit an existing tag, the bot will use the POST http method to communicate with the API. Since we're not posting a new tag, but editing an existing entry, the API will reject this request. 

Instead of using POST, we should be using PATCH, since we're only partially updating the entry in the database.

Changed files:
bot/cogs/tags.py

diff --git a/bot/cogs/tags.py b/bot/cogs/tags.py
index b9dd3595..cd70e783 100644
--- a/bot/cogs/tags.py
+++ b/bot/cogs/tags.py
@@ -86,7 +86,7 @@ class Tags(Cog):
                     max_lines=15
                 )
 
-    @tags_group.command(name='set', aliases=('add', 'edit', 's'))
+    @tags_group.command(name='set', aliases=('add', 's'))
     @with_role(*MODERATION_ROLES)
     async def set_command(
         self,
@@ -95,7 +95,7 @@ class Tags(Cog):
         *,
         tag_content: TagContentConverter,
     ) -> None:
-        """Create a new tag or update an existing one."""
+        """Create a new tag."""
         body = {
             'title': tag_name.lower().strip(),
             'embed': {
@@ -116,6 +116,35 @@ class Tags(Cog):
             colour=Colour.blurple()
         ))
 
+    @tags_group.command(name='edit', aliases=('e', ))
+    @with_role(*MODERATION_ROLES)
+    async def edit_command(
+        self,
+        ctx: Context,
+        tag_name: TagNameConverter,
+        *,
+        tag_content: TagContentConverter,
+    ) -> None:
+        """Edit an existing tag."""
+        body = {
+            'embed': {
+                'title': tag_name,
+                'description': tag_content
+            }
+        }
+
+        await self.bot.api_client.patch(f'bot/tags/{tag_name}', json=body)
+
+        log.debug(f"{ctx.author} successfully edited the following tag in our database: \\n"
+                  f"tag_name: {tag_name}\\n"
+                  f"tag_content: '{tag_content}'\\n")
+
+        await ctx.send(embed=Embed(
+            title="Tag successfully edited",
+            description=f"**{tag_name}** edited in the database.",
+            colour=Colour.blurple()
+        ))
+
     @tags_group.command(name='delete', aliases=('remove', 'rm', 'd'))
     @with_role(Roles.admin, Roles.owner)
     async def delete_command(self, ctx: Context, *, tag_name: TagNameConverter) -> None:
Repository: python-discord/bot
Issue: Verify command shouldn't log tracebacks for DM failure.

Sentry Issue: [BOT-11](https://sentry.io/organizations/python-discord/issues/1526042507/?referrer=github_integration)

It currently uses `log.exception()` for all exceptions. `Forbidden` should be a special case and use a lower log level. A better approach may be to only handle `Forbidden` and re-raise all other exceptions (to defer it to the default command error handler). A `finally` block can be used to still delete the message afterwards.

```py
        try:
            await ctx.author.send(WELCOME_MESSAGE)
        except Exception:
            # Catch the exception, in case they have DMs off or something
            log.exception(f"Unable to send welcome message to user {ctx.author}.")
```

```
Forbidden: 403 FORBIDDEN (error code: 50007): Cannot send messages to this user
  File "bot/cogs/verification.py", line 129, in accept_command
    await ctx.author.send(WELCOME_MESSAGE)
  File "discord/abc.py", line 856, in send
    data = await state.http.send_message(channel.id, content, tts=tts, embed=embed, nonce=nonce)
  File "discord/http.py", line 216, in request
    raise Forbidden(r, data)

Unable to send welcome message to user <REDACTED>.
```

Changed files:
bot/cogs/verification.py

diff --git a/bot/cogs/verification.py b/bot/cogs/verification.py
index 988e0d49..f13ccd72 100644
--- a/bot/cogs/verification.py
+++ b/bot/cogs/verification.py
@@ -1,7 +1,8 @@
 import logging
+from contextlib import suppress
 from datetime import datetime
 
-from discord import Colour, Message, NotFound, Object
+from discord import Colour, Forbidden, Message, NotFound, Object
 from discord.ext import tasks
 from discord.ext.commands import Cog, Context, command
 
@@ -127,17 +128,13 @@ class Verification(Cog):
         await ctx.author.add_roles(Object(Roles.verified), reason="Accepted the rules")
         try:
             await ctx.author.send(WELCOME_MESSAGE)
-        except Exception:
-            # Catch the exception, in case they have DMs off or something
-            log.exception(f"Unable to send welcome message to user {ctx.author}.")
-
-        log.trace(f"Deleting the message posted by {ctx.author}.")
-
-        try:
-            self.mod_log.ignore(Event.message_delete, ctx.message.id)
-            await ctx.message.delete()
-        except NotFound:
-            log.trace("No message found, it must have been deleted by another bot.")
+        except Forbidden:
+            log.info(f"Sending welcome message failed for {ctx.author}.")
+        finally:
+            log.trace(f"Deleting accept message by {ctx.author}.")
+            with suppress(NotFound):
+                self.mod_log.ignore(Event.message_delete, ctx.message.id)
+                await ctx.message.delete()
 
     @command(name='subscribe')
     @in_channel(Channels.bot)
Repository: python-discord/bot
Issue: Reduce logging level for tag cooldowns

Sentry Issue: [BOT-19](https://sentry.io/organizations/python-discord/issues/1527377135/?referrer=github_integration)

```
<REDACTED> tried to get the 'ask' tag, but the tag is on cooldown. Cooldown ends in 2.8 seconds.
```

Changed files:
bot/cogs/tags.py

diff --git a/bot/cogs/tags.py b/bot/cogs/tags.py
index 54a51921..b6360dfa 100644
--- a/bot/cogs/tags.py
+++ b/bot/cogs/tags.py
@@ -116,8 +116,10 @@ class Tags(Cog):
 
         if _command_on_cooldown(tag_name):
             time_left = Cooldowns.tags - (time.time() - self.tag_cooldowns[tag_name]["time"])
-            log.warning(f"{ctx.author} tried to get the '{tag_name}' tag, but the tag is on cooldown. "
-                        f"Cooldown ends in {time_left:.1f} seconds.")
+            log.info(
+                f"{ctx.author} tried to get the '{tag_name}' tag, but the tag is on cooldown. "
+                f"Cooldown ends in {time_left:.1f} seconds."
+            )
             return
 
         await self._get_tags()
Repository: python-discord/bot
Issue: AttributeError: 'User' object has no attribute 'add_roles'

Sentry Issue: [BOT-1N6](https://sentry.io/organizations/python-discord/issues/2661278228/?referrer=github_integration)

```
AttributeError: 'User' object has no attribute 'add_roles'
(1 additional frame(s) were not displayed)
...
  File "bot/exts/help_channels/_cog.py", line 477, in on_message
    await self.claim_channel(message)
  File "bot/utils/lock.py", line 108, in wrapper
    except discord.Forbidden:
  File "bot/utils/lock.py", line 108, in wrapper
    except discord.Forbidden:
  File "bot/utils/lock.py", line 108, in wrapper
    except discord.Forbidden:
  File "bot/exts/help_channels/_cog.py", line 128, in claim_channel
    await self._handle_role_change(message.author, message.author.add_roles)

Unhandled exception in on_message.
```

This happened as the user was banned from the server in time time between sending a message to open the help channel, and this line of code being hit.

Changed files:
bot/exts/help_channels/_cog.py

diff --git a/bot/exts/help_channels/_cog.py b/bot/exts/help_channels/_cog.py
index 498305b4..3c6cf7f2 100644
--- a/bot/exts/help_channels/_cog.py
+++ b/bot/exts/help_channels/_cog.py
@@ -125,14 +125,21 @@ class HelpChannels(commands.Cog):
         """
         log.info(f"Channel #{message.channel} was claimed by `{message.author.id}`.")
         await self.move_to_in_use(message.channel)
-        await self._handle_role_change(message.author, message.author.add_roles)
 
-        await _message.pin(message)
+        # Handle odd edge case of `message.author` not being a `discord.Member` (see bot#1839)
+        if not isinstance(message.author, discord.Member):
+            log.warning(
+                f"{message.author} ({message.author.id}) isn't a member. Not giving cooldown role or sending DM."
+            )
+        else:
+            await self._handle_role_change(message.author, message.author.add_roles)
 
-        try:
-            await _message.dm_on_open(message)
-        except Exception as e:
-            log.warning("Error occurred while sending DM:", exc_info=e)
+            try:
+                await _message.dm_on_open(message)
+            except Exception as e:
+                log.warning("Error occurred while sending DM:", exc_info=e)
+
+        await _message.pin(message)
 
         # Add user with channel for dormant check.
         await _caches.claimants.set(message.channel.id, message.author.id)
Repository: python-discord/bot
Issue: `!doc setdoc` gives malformed request error when setting a package that already exists

If you run, for example
```python
!doc setdoc pygame https://www.pygame.org/docs/objects.inv
```
twice, you'll get a message saying `According to the API, your request is malformed.` and this error:
```python
bot_1        | 2021-10-05 18:04:59 | bot.exts.backend.error_handler | DEBUG | API responded with 400 for command docs setdoc: {'package': ['documentation link with this package already exists.']}.
```
We should handle this and give the user a better error message

Changed files:
bot/exts/info/doc/_cog.py

diff --git a/bot/exts/info/doc/_cog.py b/bot/exts/info/doc/_cog.py
index e7710db2..ca6af946 100644
--- a/bot/exts/info/doc/_cog.py
+++ b/bot/exts/info/doc/_cog.py
@@ -13,6 +13,7 @@ import aiohttp
 import discord
 from discord.ext import commands
 
+from bot.api import ResponseCodeError
 from bot.bot import Bot
 from bot.constants import MODERATION_ROLES, RedirectOutput
 from bot.converters import Inventory, PackageName, ValidURL, allowed_strings
@@ -395,7 +396,14 @@ class DocCog(commands.Cog):
             "base_url": base_url,
             "inventory_url": inventory_url
         }
-        await self.bot.api_client.post("bot/documentation-links", json=body)
+        try:
+            await self.bot.api_client.post("bot/documentation-links", json=body)
+        except ResponseCodeError as err:
+            if err.status == 400 and "already exists" in err.response_json.get("package", [""])[0]:
+                log.info(f"Ignoring HTTP 400 as package {package_name} has already been added.")
+                await ctx.send(f"Package {package_name} has already been added.")
+                return
+            raise
 
         log.info(
             f"User @{ctx.author} ({ctx.author.id}) added a new documentation package:\\n"
Repository: python-discord/bot
Issue: Help command raises AttributeError when argument is a cog name

For example, `!help Moderation` returns

```
AttributeError: 'Command' object has no attribute 'instance'
```

Have not looked into the cause of this yet.

Changed files:
bot/cogs/watchchannels/watchchannel.py, bot/cogs/help.py

diff --git a/bot/cogs/help.py b/bot/cogs/help.py
index 4971cd0b..37d12b2d 100644
--- a/bot/cogs/help.py
+++ b/bot/cogs/help.py
@@ -110,9 +110,9 @@ class HelpSession:
         cog = self._bot.cogs.get(query)
         if cog:
             return Cog(
-                name=cog.__class__.__name__,
+                name=cog.qualified_name,
                 description=inspect.getdoc(cog),
-                commands=[c for c in self._bot.commands if c.instance is cog]
+                commands=[c for c in self._bot.commands if c.cog is cog]
             )
 
         self._handle_not_found(query)
diff --git a/bot/cogs/watchchannels/watchchannel.py b/bot/cogs/watchchannels/watchchannel.py
index 122e3dfe..ce8014d6 100644
--- a/bot/cogs/watchchannels/watchchannel.py
+++ b/bot/cogs/watchchannels/watchchannel.py
@@ -148,8 +148,8 @@ class WatchChannel(metaclass=CogABCMeta):
                 title=f"Warning: Failed to retrieve user cache for the {self.__class__.__name__} watch channel",
                 text="Could not retrieve the list of watched users from the API and messages will not be relayed.",
                 ping_everyone=True,
-                icon=Icons.token_removed,
-                color=Color.red()
+                icon_url=Icons.token_removed,
+                colour=Color.red()
             )
 
     async def fetch_user_cache(self) -> bool:
Repository: python-discord/bot
Issue: Pep command cannot fetch PEP 0.

The [PEP 0](https://www.python.org/dev/peps/) is an index of all the other PEPs;
instead of being in the github repo directly, it's generated automatically through a script present there.
~~If implemented, scraping the site for it seems to be the better option as generating it would require the whole up to date repo.~~
The header is a constant in the pep0 package and can be gotten from there in case of future changes 
 or as suggested in the comment below https://github.com/python/peps/blob/master/pep0/constants.py#L10-L20

Changed files:
bot/cogs/utils.py

diff --git a/bot/cogs/utils.py b/bot/cogs/utils.py
index 024141d6..f0b1172e 100644
--- a/bot/cogs/utils.py
+++ b/bot/cogs/utils.py
@@ -40,6 +40,8 @@ If the implementation is easy to explain, it may be a good idea.
 Namespaces are one honking great idea -- let's do more of those!
 """
 
+ICON_URL = "https://www.python.org/static/opengraph-icon-200x200.png"
+
 
 class Utils(Cog):
     """A selection of utilities which don't have a clear category."""
@@ -59,6 +61,10 @@ class Utils(Cog):
             await ctx.invoke(self.bot.get_command("help"), "pep")
             return
 
+        # Handle PEP 0 directly because it's not in .rst or .txt so it can't be accessed like other PEPs.
+        if pep_number == 0:
+            return await self.send_pep_zero(ctx)
+
         possible_extensions = ['.txt', '.rst']
         found_pep = False
         for extension in possible_extensions:
@@ -82,7 +88,7 @@ class Utils(Cog):
                     description=f"[Link]({self.base_pep_url}{pep_number:04})",
                 )
 
-                pep_embed.set_thumbnail(url="https://www.python.org/static/opengraph-icon-200x200.png")
+                pep_embed.set_thumbnail(url=ICON_URL)
 
                 # Add the interesting information
                 fields_to_check = ("Status", "Python-Version", "Created", "Type")
@@ -278,6 +284,19 @@ class Utils(Cog):
         for reaction in options:
             await message.add_reaction(reaction)
 
+    async def send_pep_zero(self, ctx: Context) -> None:
+        """Send information about PEP 0."""
+        pep_embed = Embed(
+            title=f"**PEP 0 - Index of Python Enhancement Proposals (PEPs)**",
+            description=f"[Link](https://www.python.org/dev/peps/)"
+        )
+        pep_embed.set_thumbnail(url=ICON_URL)
+        pep_embed.add_field(name="Status", value="Active")
+        pep_embed.add_field(name="Created", value="13-Jul-2000")
+        pep_embed.add_field(name="Type", value="Informational")
+
+        await ctx.send(embed=pep_embed)
+
 
 def setup(bot: Bot) -> None:
     """Load the Utils cog."""
Repository: python-discord/bot
Issue: !watch alias is not working.

The `!watch` alias broke when we changed the watch command to take a note instead of a channel - this is due to converters in the alias. I'll fix it.

Changed files:
bot/cogs/alias.py

diff --git a/bot/cogs/alias.py b/bot/cogs/alias.py
index 12edb202..2ce4a51e 100644
--- a/bot/cogs/alias.py
+++ b/bot/cogs/alias.py
@@ -1,7 +1,7 @@
 import inspect
 import logging
 
-from discord import Colour, Embed, TextChannel, User
+from discord import Colour, Embed, User
 from discord.ext.commands import (
     Command, Context, clean_content, command, group
 )
@@ -71,13 +71,13 @@ class Alias:
 
     @command(name="watch", hidden=True)
     async def bigbrother_watch_alias(
-            self, ctx, user: User, channel: TextChannel = None
+            self, ctx, user: User, *, reason: str = None
     ):
         """
         Alias for invoking <prefix>bigbrother watch user [text_channel].
         """
 
-        await self.invoke(ctx, "bigbrother watch", user, channel)
+        await self.invoke(ctx, "bigbrother watch", user, reason=reason)
 
     @command(name="unwatch", hidden=True)
     async def bigbrother_unwatch_alias(self, ctx, user: User):
Repository: python-discord/bot
Issue: Helpers can’t mention others in reminders

Helpers (and partners/python community) should be able to mention other users, but not roles, in reminders. In late June, this was still possible, but it currently isn't.

While looking through the source code and git blame (looks *calmly* at @TizzySaurus), I think I managed to locate the issue to the `Reminders._check_mentions` method. For a non-mod helper, the error message when mentioning another user in a reminder (should be allowed), is the error message that should be used when trying to mention a role (defined at https://github.com/python-discord/bot/blob/06e90af5e19d9dd83ae44ac2ae0eb3eeb43349e2/bot/exts/utils/reminders.py#L120).

It basically seems like the user is getting blocked due to not being a moderator, while they should be allowed to mention others. If someone could look at this a bit more extensively than I did today, and fix it, that would be much appreciated 😄

Changed files:
bot/exts/utils/reminders.py

diff --git a/bot/exts/utils/reminders.py b/bot/exts/utils/reminders.py
index be518129..b4f88af0 100644
--- a/bot/exts/utils/reminders.py
+++ b/bot/exts/utils/reminders.py
@@ -118,7 +118,7 @@ class Reminders(Cog):
         if await has_no_roles_check(ctx, *STAFF_PARTNERS_COMMUNITY_ROLES):
             return False, "members/roles"
         elif await has_no_roles_check(ctx, *MODERATION_ROLES):
-            return all(isinstance(mention, discord.Member) for mention in mentions), "roles"
+            return all(isinstance(mention, (discord.User, discord.Member)) for mention in mentions), "roles"
         else:
             return True, ""
Repository: python-discord/bot
Issue: Too long rule number causes an Invalid Form Body error

Sentry Issue: [BOT-1QT](https://sentry.io/organizations/python-discord/issues/2809844874/?referrer=github_integration)

```
HTTPException: 400 Bad Request (error code: 50035): Invalid Form Body
In content: Must be 2000 or fewer in length.
  File "discord/ext/commands/core.py", line 167, in wrapped
    ret = await coro(*args, **kwargs)
  File "bot/exts/info/site.py", line 129, in site_rules
    await ctx.send(f":x: Invalid rule indices: {invalid}")
  File "discord/abc.py", line 1422, in send
    data = await state.http.send_message(
  File "discord/http.py", line 335, in request
    raise HTTPException(response, data)

Error executing command invoked by [READCTED]: !rule 100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000...
```

Changed files:
bot/exts/info/site.py

diff --git a/bot/exts/info/site.py b/bot/exts/info/site.py
index e8e71558..bcb04c90 100644
--- a/bot/exts/info/site.py
+++ b/bot/exts/info/site.py
@@ -1,3 +1,5 @@
+from textwrap import shorten
+
 from discord import Colour, Embed
 from discord.ext.commands import Cog, Context, Greedy, group
 
@@ -123,7 +125,8 @@ class Site(Cog):
 
         # Remove duplicates and sort the rule indices
         rules = sorted(set(rules))
-        invalid = ', '.join(str(index) for index in rules if index < 1 or index > len(full_rules))
+        invalid = shorten(', '.join(str(index) for index in rules if index
+                          < 1 or index > len(full_rules)), 50, placeholder='...')
 
         if invalid:
             await ctx.send(f":x: Invalid rule indices: {invalid}")
Repository: python-discord/bot
Issue: Clear Doc Cache Raises Typerror

Sentry Issue: [BOT-2JN](https://sentry.io/organizations/python-discord/issues/2876011673/?referrer=github_integration)

```
TypeError: delete() missing 1 required positional argument: 'package'
  File "discord/ext/commands/core.py", line 167, in wrapped
    ret = await coro(*args, **kwargs)
  File "bot/exts/info/doc/_cog.py", line 467, in clear_cache_command
    await self.item_fetcher.stale_inventory_notifier.symbol_counter.delete()
  File "async_rediscache/types/base.py", line 243, in wrapper
    coroutine_object = method(self, *args, **kwargs)
```

Changed files:
bot/exts/info/doc/_cog.py

diff --git a/bot/exts/info/doc/_cog.py b/bot/exts/info/doc/_cog.py
index ebf5f593..4dc5276d 100644
--- a/bot/exts/info/doc/_cog.py
+++ b/bot/exts/info/doc/_cog.py
@@ -464,7 +464,7 @@ class DocCog(commands.Cog):
     ) -> None:
         """Clear the persistent redis cache for `package`."""
         if await doc_cache.delete(package_name):
-            await self.item_fetcher.stale_inventory_notifier.symbol_counter.delete()
+            await self.item_fetcher.stale_inventory_notifier.symbol_counter.delete(package_name)
             await ctx.send(f"Successfully cleared the cache for `{package_name}`.")
         else:
             await ctx.send("No keys matching the package found.")
Repository: python-discord/bot
Issue: Cannot suppress embeds in a DM channel

Sentry Issue: [BOT-2Y7](https://sentry.io/organizations/python-discord/issues/3011204281/?referrer=github_integration)

```
Forbidden: 403 Forbidden (error code: 50003): Cannot execute action on a DM channel
  File "discord/client.py", line 351, in _run_event
    await coro(*args, **kwargs)
  File "bot/exts/info/code_snippets.py", line 254, in on_message
    await message.edit(suppress=True)
  File "discord/message.py", line 1289, in edit
    data = await self._state.http.edit_message(self.channel.id, self.id, **payload)
  File "discord/http.py", line 329, in request
    raise Forbidden(response, data)

Unhandled exception in on_message.
```

This seems to be caused by the code snippet cog trying to remove the embed from the user's message, but doesn't have permissions to in DMs.  

Changed files:
bot/exts/info/code_snippets.py

diff --git a/bot/exts/info/code_snippets.py b/bot/exts/info/code_snippets.py
index 07b1b8a2..f2f29020 100644
--- a/bot/exts/info/code_snippets.py
+++ b/bot/exts/info/code_snippets.py
@@ -246,6 +246,9 @@ class CodeSnippets(Cog):
         if message.author.bot:
             return
 
+        if message.guild is None:
+            return
+
         message_to_send = await self._parse_snippets(message.content)
         destination = message.channel