aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.pre-commit-config.yaml25
-rw-r--r--CONTRIBUTING.md2
-rw-r--r--Pipfile3
-rw-r--r--Pipfile.lock41
-rw-r--r--azure-pipelines.yml24
-rw-r--r--bot/cogs/antimalware.py41
-rw-r--r--bot/cogs/extensions.py6
-rw-r--r--bot/cogs/free.py2
-rw-r--r--bot/cogs/watchchannels/watchchannel.py11
-rw-r--r--bot/utils/__init__.py57
-rw-r--r--tests/README.md10
-rw-r--r--tests/base.py4
-rw-r--r--tests/bot/cogs/sync/test_base.py2
-rw-r--r--tests/bot/cogs/test_snekbox.py6
-rw-r--r--tests/bot/test_utils.py37
15 files changed, 121 insertions, 150 deletions
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 860357868..f369fb7d1 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,10 +1,27 @@
+exclude: ^\.cache/|\.venv/|\.git/|htmlcov/|logs/
repos:
-- repo: local
+ - repo: https://github.com/pre-commit/pre-commit-hooks
+ rev: v2.5.0
hooks:
- - id: flake8
+ - id: check-merge-conflict
+ - id: check-toml
+ - id: check-yaml
+ args: [--unsafe] # Required due to custom constructors (e.g. !ENV)
+ - id: end-of-file-fixer
+ - id: mixed-line-ending
+ args: [--fix=lf]
+ - id: trailing-whitespace
+ args: [--markdown-linebreak-ext=md]
+ - repo: https://github.com/pre-commit/pygrep-hooks
+ rev: v1.5.1
+ hooks:
+ - id: python-check-blanket-noqa
+ - repo: local
+ hooks:
+ - id: flake8
name: Flake8
description: This hook runs flake8 within our project's pipenv environment.
- entry: pipenv run lint
+ entry: pipenv run flake8
language: python
types: [python]
- require_serial: true \ No newline at end of file
+ require_serial: true
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 39f76c7b4..61d11f844 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -43,7 +43,7 @@ To provide a standalone development environment for this project, docker compose
When pulling down changes from GitHub, remember to sync your environment using `pipenv sync --dev` to ensure you're using the most up-to-date versions the project's dependencies.
### Type Hinting
-[PEP 484](https://www.python.org/dev/peps/pep-0484/) formally specifies type hints for Python functions, added to the Python Standard Library in version 3.5. Type hints are recognized by most modern code editing tools and provide useful insight into both the input and output types of a function, preventing the user from having to go through the codebase to determine these types.
+[PEP 484](https://www.python.org/dev/peps/pep-0484/) formally specifies type hints for Python functions, added to the Python Standard Library in version 3.5. Type hints are recognized by most modern code editing tools and provide useful insight into both the input and output types of a function, preventing the user from having to go through the codebase to determine these types.
For example:
diff --git a/Pipfile b/Pipfile
index 64760f9dd..0dcee0e3d 100644
--- a/Pipfile
+++ b/Pipfile
@@ -31,6 +31,7 @@ flake8-import-order = "~=0.18"
flake8-string-format = "~=0.2"
flake8-tidy-imports = "~=4.0"
flake8-todo = "~=0.7"
+pep8-naming = "~=0.9"
pre-commit = "~=2.1"
safety = "~=1.8"
unittest-xml-reporting = "~=3.0"
@@ -41,7 +42,7 @@ python_version = "3.8"
[scripts]
start = "python -m bot"
-lint = "python -m flake8"
+lint = "pre-commit run --all-files"
precommit = "pre-commit install"
build = "docker build -t pythondiscord/bot:latest -f Dockerfile ."
push = "docker push pythondiscord/bot:latest"
diff --git a/Pipfile.lock b/Pipfile.lock
index 9953aab40..348456f2c 100644
--- a/Pipfile.lock
+++ b/Pipfile.lock
@@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
- "sha256": "fae6dcdb6a5ebf27e8ea5044f4ca2ab854774d17affb5fd64ac85f8d0ae71187"
+ "sha256": "b8b38e84230bdc37f8c8955e8dddc442183a2e23c4dfc6ed37c522644aecdeea"
},
"pipfile-spec": 6,
"requires": {
@@ -18,11 +18,11 @@
"default": {
"aio-pika": {
"hashes": [
- "sha256:4199122a450dffd8303b7857a9d82657bf1487fe329e489520833b40fbe92406",
- "sha256:fe85c7456e5c060bce4eb9cffab5b2c4d3c563cb72177977b3556c54c8e3aeb6"
+ "sha256:0332bc13abbd8923dac657b331716778c55ea0a32ac0951306ce85edafcc916c",
+ "sha256:39770d8bc7e9059e28622d599e2ac9ebc16a7198b33d1743c1a496ca3b0f8170"
],
"index": "pypi",
- "version": "==6.5.2"
+ "version": "==6.5.3"
},
"aiodns": {
"hashes": [
@@ -189,10 +189,10 @@
},
"humanfriendly": {
"hashes": [
- "sha256:cbe04ecf964ccb951a578f396091f258448ca4b4b4c6d4b6194f48ef458fe991",
- "sha256:e8e2e4524409e55d5c5cbbb4c555a0c0a9599d5e8f74d0ce1ac504ba51ad1cd2"
+ "sha256:2f79aaa2965c0fc3d79452e64ec2c7601d70d67e51ea2e99cb40afe3fe2824c5",
+ "sha256:6990c0af4b72f50ddf302900eb982edf199247e621e06d80d71b00b1a1574214"
],
- "version": "==7.2"
+ "version": "==8.0"
},
"idna": {
"hashes": [
@@ -379,8 +379,7 @@
},
"pycparser": {
"hashes": [
- "sha256:a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3",
- "sha256:fd64020e8a5e0369de455adf9f22795a90fdb74e6bb999e9a13fd26b54f533ef"
+ "sha256:a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3"
],
"version": "==2.19"
},
@@ -663,8 +662,7 @@
},
"distlib": {
"hashes": [
- "sha256:2e166e231a26b36d6dfe35a48c4464346620f8645ed0ace01ee31822b288de21",
- "sha256:9b183fb98f4870e02d315d5d17baef14be74c339d827346cae544f5597698555"
+ "sha256:2e166e231a26b36d6dfe35a48c4464346620f8645ed0ace01ee31822b288de21"
],
"version": "==0.3.0"
},
@@ -707,11 +705,11 @@
},
"flake8-annotations": {
"hashes": [
- "sha256:19a6637a5da1bb7ea7948483ca9e2b9e15b213e687e7bf5ff8c1bfc91c185006",
- "sha256:bb033b72cdd3a2b0a530bbdf2081f12fbea7d70baeaaebb5899723a45f424b8e"
+ "sha256:a38b44d01abd480586a92a02a2b0a36231ec42dcc5e114de78fa5db016d8d3f9",
+ "sha256:d5b0e8704e4e7728b352fa1464e23539ff2341ba11cc153b536fa2cf921ee659"
],
"index": "pypi",
- "version": "==2.0.0"
+ "version": "==2.0.1"
},
"flake8-bugbear": {
"hashes": [
@@ -737,6 +735,13 @@
"index": "pypi",
"version": "==0.18.1"
},
+ "flake8-polyfill": {
+ "hashes": [
+ "sha256:12be6a34ee3ab795b19ca73505e7b55826d5f6ad7230d31b18e106400169b9e9",
+ "sha256:e44b087597f6da52ec6393a709e7108b2905317d0c0b744cdca6208e670d8eda"
+ ],
+ "version": "==1.0.2"
+ },
"flake8-string-format": {
"hashes": [
"sha256:65f3da786a1461ef77fca3780b314edb2853c377f2e35069723348c8917deaa2",
@@ -794,6 +799,14 @@
],
"version": "==20.1"
},
+ "pep8-naming": {
+ "hashes": [
+ "sha256:45f330db8fcfb0fba57458c77385e288e7a3be1d01e8ea4268263ef677ceea5f",
+ "sha256:a33d38177056321a167decd6ba70b890856ba5025f0a8eca6a3eda607da93caf"
+ ],
+ "index": "pypi",
+ "version": "==0.9.1"
+ },
"pre-commit": {
"hashes": [
"sha256:09ebe467f43ce24377f8c2f200fe3cd2570d328eb2ce0568c8e96ce19da45fa6",
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index 35dea089a..280f11a36 100644
--- a/azure-pipelines.yml
+++ b/azure-pipelines.yml
@@ -13,10 +13,12 @@ jobs:
variables:
PIP_CACHE_DIR: ".cache/pip"
+ PRE_COMMIT_HOME: $(Pipeline.Workspace)/pre-commit-cache
steps:
- task: UsePythonVersion@0
displayName: 'Set Python version'
+ name: PythonVersion
inputs:
versionSpec: '3.8.x'
addToPath: true
@@ -27,8 +29,26 @@ jobs:
- script: pipenv install --dev --deploy --system
displayName: 'Install project using pipenv'
- - script: python -m flake8
- displayName: 'Run linter'
+ # Create an executable shell script which replaces the original pipenv binary.
+ # The shell script ignores the first argument and executes the rest of the args as a command.
+ # It makes the `pipenv run flake8` command in the pre-commit hook work by circumventing
+ # pipenv entirely, which is too dumb to know it should use the system interpreter rather than
+ # creating a new venv.
+ - script: |
+ printf '%s\n%s' '#!/bin/bash' '"${@:2}"' > $(PythonVersion.pythonLocation)/bin/pipenv \
+ && chmod +x $(PythonVersion.pythonLocation)/bin/pipenv
+ displayName: 'Mock pipenv binary'
+
+ - task: Cache@2
+ displayName: 'Restore pre-commit environment'
+ inputs:
+ key: pre-commit | "$(PythonVersion.pythonLocation)" | .pre-commit-config.yaml
+ restoreKeys: |
+ pre-commit | "$(PythonVersion.pythonLocation)"
+ path: $(PRE_COMMIT_HOME)
+
+ - script: pre-commit run --all-files --show-diff-on-failure
+ displayName: 'Run pre-commit hooks'
- script: BOT_API_KEY=foo BOT_SENTRY_DSN=blah BOT_TOKEN=bar WOLFRAM_API_KEY=baz REDDIT_CLIENT_ID=spam REDDIT_SECRET=ham coverage run -m xmlrunner
displayName: Run tests
diff --git a/bot/cogs/antimalware.py b/bot/cogs/antimalware.py
index 9e9e81364..373619895 100644
--- a/bot/cogs/antimalware.py
+++ b/bot/cogs/antimalware.py
@@ -1,4 +1,5 @@
import logging
+from os.path import splitext
from discord import Embed, Message, NotFound
from discord.ext.commands import Cog
@@ -28,24 +29,30 @@ class AntiMalware(Cog):
return
embed = Embed()
- for attachment in message.attachments:
- filename = attachment.filename.lower()
- if filename.endswith('.py'):
- embed.description = (
- f"It looks like you tried to attach a Python file - please "
- f"use a code-pasting service such as {URLs.site_schema}{URLs.site_paste}"
- )
- break # Other detections irrelevant because we prioritize the .py message.
- if not filename.endswith(tuple(AntiMalwareConfig.whitelist)):
- whitelisted_types = ', '.join(AntiMalwareConfig.whitelist)
- meta_channel = self.bot.get_channel(Channels.meta)
- embed.description = (
- f"It looks like you tried to attach a file type that we "
- f"do not allow. We currently allow the following file "
- f"types: **{whitelisted_types}**. \n\n Feel free to ask "
- f"in {meta_channel.mention} if you think this is a mistake."
- )
+ file_extensions = {splitext(message.filename.lower())[1] for message in message.attachments}
+ extensions_blocked = file_extensions - set(AntiMalwareConfig.whitelist)
+ if ".py" in extensions_blocked:
+ # Short-circuit on *.py files to provide a pastebin link
+ embed.description = (
+ "It looks like you tried to attach a Python file - "
+ f"please use a code-pasting service such as {URLs.site_schema}{URLs.site_paste}"
+ )
+ elif extensions_blocked:
+ blocked_extensions_str = ', '.join(extensions_blocked)
+ whitelisted_types = ', '.join(AntiMalwareConfig.whitelist)
+ meta_channel = self.bot.get_channel(Channels.meta)
+ embed.description = (
+ f"It looks like you tried to attach file type(s) that we do not allow ({blocked_extensions_str}). "
+ f"We currently allow the following file types: **{whitelisted_types}**.\n\n"
+ f"Feel free to ask in {meta_channel.mention} if you think this is a mistake."
+ )
+
if embed.description:
+ log.info(
+ f"User '{message.author}' ({message.author.id}) uploaded blacklisted file(s): {blocked_extensions_str}",
+ extra={"attachment_list": [attachment.filename for attachment in message.attachments]}
+ )
+
await message.channel.send(f"Hey {message.author.mention}!", embed=embed)
# Delete the offending message:
diff --git a/bot/cogs/extensions.py b/bot/cogs/extensions.py
index b312e1a1d..fb6cd9aa3 100644
--- a/bot/cogs/extensions.py
+++ b/bot/cogs/extensions.py
@@ -69,7 +69,7 @@ class Extensions(commands.Cog):
@extensions_group.command(name="load", aliases=("l",))
async def load_command(self, ctx: Context, *extensions: Extension) -> None:
- """
+ r"""
Load extensions given their fully qualified or unqualified names.
If '\*' or '\*\*' is given as the name, all unloaded extensions will be loaded.
@@ -86,7 +86,7 @@ class Extensions(commands.Cog):
@extensions_group.command(name="unload", aliases=("ul",))
async def unload_command(self, ctx: Context, *extensions: Extension) -> None:
- """
+ r"""
Unload currently loaded extensions given their fully qualified or unqualified names.
If '\*' or '\*\*' is given as the name, all loaded extensions will be unloaded.
@@ -109,7 +109,7 @@ class Extensions(commands.Cog):
@extensions_group.command(name="reload", aliases=("r",))
async def reload_command(self, ctx: Context, *extensions: Extension) -> None:
- """
+ r"""
Reload extensions given their fully qualified or unqualified names.
If an extension fails to be reloaded, it will be rolled-back to the prior working state.
diff --git a/bot/cogs/free.py b/bot/cogs/free.py
index 02c02d067..33b55e79a 100644
--- a/bot/cogs/free.py
+++ b/bot/cogs/free.py
@@ -55,7 +55,7 @@ class Free(Cog):
msg = messages[seek - 1]
# Otherwise get last message
else:
- msg = await channel.history(limit=1).next() # noqa (False positive)
+ msg = await channel.history(limit=1).next() # noqa: B305
inactive = (datetime.utcnow() - msg.created_at).seconds
if inactive > TIMEOUT:
diff --git a/bot/cogs/watchchannels/watchchannel.py b/bot/cogs/watchchannels/watchchannel.py
index 3667a80e8..479820444 100644
--- a/bot/cogs/watchchannels/watchchannel.py
+++ b/bot/cogs/watchchannels/watchchannel.py
@@ -9,7 +9,7 @@ from typing import Optional
import dateutil.parser
import discord
-from discord import Color, Embed, HTTPException, Message, errors
+from discord import Color, DMChannel, Embed, HTTPException, Message, errors
from discord.ext.commands import Cog, Context
from bot.api import ResponseCodeError
@@ -273,7 +273,14 @@ class WatchChannel(metaclass=CogABCMeta):
reason = self.watched_users[user_id]['reason']
- embed = Embed(description=f"{msg.author.mention} in [#{msg.channel.name}]({msg.jump_url})")
+ if isinstance(msg.channel, DMChannel):
+ # If a watched user DMs the bot there won't be a channel name or jump URL
+ # This could technically include a GroupChannel but bot's can't be in those
+ message_jump = "via DM"
+ else:
+ message_jump = f"in [#{msg.channel.name}]({msg.jump_url})"
+
+ embed = Embed(description=f"{msg.author.mention} {message_jump}")
embed.set_footer(text=f"Added {time_delta} by {actor} | Reason: {reason}")
await self.webhook_send(embed=embed, username=msg.author.display_name, avatar_url=msg.author.avatar_url)
diff --git a/bot/utils/__init__.py b/bot/utils/__init__.py
index 3e4b15ce4..9b32e515d 100644
--- a/bot/utils/__init__.py
+++ b/bot/utils/__init__.py
@@ -1,5 +1,4 @@
from abc import ABCMeta
-from typing import Any, Hashable
from discord.ext.commands import CogMeta
@@ -8,59 +7,3 @@ class CogABCMeta(CogMeta, ABCMeta):
"""Metaclass for ABCs meant to be implemented as Cogs."""
pass
-
-
-class CaseInsensitiveDict(dict):
- """
- We found this class on StackOverflow. Thanks to m000 for writing it!
-
- https://stackoverflow.com/a/32888599/4022104
- """
-
- @classmethod
- def _k(cls, key: Hashable) -> Hashable:
- """Return lowered key if a string-like is passed, otherwise pass key straight through."""
- return key.lower() if isinstance(key, str) else key
-
- def __init__(self, *args, **kwargs):
- super(CaseInsensitiveDict, self).__init__(*args, **kwargs)
- self._convert_keys()
-
- def __getitem__(self, key: Hashable) -> Any:
- """Case insensitive __setitem__."""
- return super(CaseInsensitiveDict, self).__getitem__(self.__class__._k(key))
-
- def __setitem__(self, key: Hashable, value: Any):
- """Case insensitive __setitem__."""
- super(CaseInsensitiveDict, self).__setitem__(self.__class__._k(key), value)
-
- def __delitem__(self, key: Hashable) -> Any:
- """Case insensitive __delitem__."""
- return super(CaseInsensitiveDict, self).__delitem__(self.__class__._k(key))
-
- def __contains__(self, key: Hashable) -> bool:
- """Case insensitive __contains__."""
- return super(CaseInsensitiveDict, self).__contains__(self.__class__._k(key))
-
- def pop(self, key: Hashable, *args, **kwargs) -> Any:
- """Case insensitive pop."""
- return super(CaseInsensitiveDict, self).pop(self.__class__._k(key), *args, **kwargs)
-
- def get(self, key: Hashable, *args, **kwargs) -> Any:
- """Case insensitive get."""
- return super(CaseInsensitiveDict, self).get(self.__class__._k(key), *args, **kwargs)
-
- def setdefault(self, key: Hashable, *args, **kwargs) -> Any:
- """Case insensitive setdefault."""
- return super(CaseInsensitiveDict, self).setdefault(self.__class__._k(key), *args, **kwargs)
-
- def update(self, E: Any = None, **F) -> None:
- """Case insensitive update."""
- super(CaseInsensitiveDict, self).update(self.__class__(E))
- super(CaseInsensitiveDict, self).update(self.__class__(**F))
-
- def _convert_keys(self) -> None:
- """Helper method to lowercase all existing string-like keys."""
- for k in list(self.keys()):
- v = super(CaseInsensitiveDict, self).pop(k)
- self.__setitem__(k, v)
diff --git a/tests/README.md b/tests/README.md
index be78821bf..4f62edd68 100644
--- a/tests/README.md
+++ b/tests/README.md
@@ -83,7 +83,7 @@ TagContentConverter should return correct values for valid input.
As we are trying to test our "units" of code independently, we want to make sure that we do not rely objects and data generated by "external" code. If we we did, then we wouldn't know if the failure we're observing was caused by the code we are actually trying to test or something external to it.
-However, the features that we are trying to test often depend on those objects generated by external pieces of code. It would be difficult to test a bot command without having access to a `Context` instance. Fortunately, there's a solution for that: we use fake objects that act like the true object. We call these fake objects "mocks".
+However, the features that we are trying to test often depend on those objects generated by external pieces of code. It would be difficult to test a bot command without having access to a `Context` instance. Fortunately, there's a solution for that: we use fake objects that act like the true object. We call these fake objects "mocks".
To create these mock object, we mainly use the [`unittest.mock`](https://docs.python.org/3/library/unittest.mock.html) module. In addition, we have also defined a couple of specialized mock objects that mock specific `discord.py` types (see the section on the below.).
@@ -114,13 +114,13 @@ class BotCogTests(unittest.TestCase):
### Mocking coroutines
-By default, the `unittest.mock.Mock` and `unittest.mock.MagicMock` classes cannot mock coroutines, since the `__call__` method they provide is synchronous. In anticipation of the `AsyncMock` that will be [introduced in Python 3.8](https://docs.python.org/3.9/whatsnew/3.8.html#unittest), we have added an `AsyncMock` helper to [`helpers.py`](/tests/helpers.py). Do note that this drop-in replacement only implements an asynchronous `__call__` method, not the additional assertions that will come with the new `AsyncMock` type in Python 3.8.
+By default, the `unittest.mock.Mock` and `unittest.mock.MagicMock` classes cannot mock coroutines, since the `__call__` method they provide is synchronous. In anticipation of the `AsyncMock` that will be [introduced in Python 3.8](https://docs.python.org/3.9/whatsnew/3.8.html#unittest), we have added an `AsyncMock` helper to [`helpers.py`](/tests/helpers.py). Do note that this drop-in replacement only implements an asynchronous `__call__` method, not the additional assertions that will come with the new `AsyncMock` type in Python 3.8.
### Special mocks for some `discord.py` types
To quote Ned Batchelder, Mock objects are "automatic chameleons". This means that they will happily allow the access to any attribute or method and provide a mocked value in return. One downside to this is that if the code you are testing gets the name of the attribute wrong, your mock object will not complain and the test may still pass.
-In order to avoid that, we have defined a number of Mock types in [`helpers.py`](/tests/helpers.py) that follow the specifications of the actual Discord types they are mocking. This means that trying to access an attribute or method on a mocked object that does not exist on the equivalent `discord.py` object will result in an `AttributeError`. In addition, these mocks have some sensible defaults and **pass `isinstance` checks for the types they are mocking**.
+In order to avoid that, we have defined a number of Mock types in [`helpers.py`](/tests/helpers.py) that follow the specifications of the actual Discord types they are mocking. This means that trying to access an attribute or method on a mocked object that does not exist on the equivalent `discord.py` object will result in an `AttributeError`. In addition, these mocks have some sensible defaults and **pass `isinstance` checks for the types they are mocking**.
These special mocks are added when they are needed, so if you think it would be sensible to add another one, feel free to propose one in your PR.
@@ -144,7 +144,7 @@ Finally, there are some considerations to make when writing tests, both for writ
### Test coverage is a starting point
-Having test coverage is a good starting point for unit testing: If a part of your code was not covered by a test, we know that we have not tested it properly. The reverse is unfortunately not true: Even if the code we are testing has 100% branch coverage, it does not mean it's fully tested or guaranteed to work.
+Having test coverage is a good starting point for unit testing: If a part of your code was not covered by a test, we know that we have not tested it properly. The reverse is unfortunately not true: Even if the code we are testing has 100% branch coverage, it does not mean it's fully tested or guaranteed to work.
One problem is that 100% branch coverage may be misleading if we haven't tested our code against all the realistic input it may get in production. For instance, take a look at the following `member_information` function and the test we've written for it:
@@ -169,7 +169,7 @@ class FunctionsTests(unittest.TestCase):
If you were to run this test, not only would the function pass the test, `coverage.py` will also tell us that the test provides 100% branch coverage for the function. Can you spot the bug the test suite did not catch?
-The problem here is that we have only tested our function with a member object that had `None` for the `member.joined` attribute. This means that `member.joined.stfptime("%d-%m-%Y")` was never executed during our test, leading to us missing the spelling mistake in `stfptime` (it should be `strftime`).
+The problem here is that we have only tested our function with a member object that had `None` for the `member.joined` attribute. This means that `member.joined.stfptime("%d-%m-%Y")` was never executed during our test, leading to us missing the spelling mistake in `stfptime` (it should be `strftime`).
Adding another test would not increase the test coverage we have, but it does ensure that we'll notice that this function can fail with realistic data:
diff --git a/tests/base.py b/tests/base.py
index 42174e911..d99b9ac31 100644
--- a/tests/base.py
+++ b/tests/base.py
@@ -31,7 +31,7 @@ class LoggingTestsMixin:
"""
@contextmanager
- def assertNotLogs(self, logger=None, level=None, msg=None):
+ def assertNotLogs(self, logger=None, level=None, msg=None): # noqa: N802
"""
Asserts that no logs of `level` and higher were emitted by `logger`.
@@ -81,7 +81,7 @@ class LoggingTestsMixin:
class CommandTestCase(unittest.IsolatedAsyncioTestCase):
"""TestCase with additional assertions that are useful for testing Discord commands."""
- async def assertHasPermissionsCheck(
+ async def assertHasPermissionsCheck( # noqa: N802
self,
cmd: commands.Command,
permissions: Dict[str, bool],
diff --git a/tests/bot/cogs/sync/test_base.py b/tests/bot/cogs/sync/test_base.py
index fe0594efe..6ee9dfda6 100644
--- a/tests/bot/cogs/sync/test_base.py
+++ b/tests/bot/cogs/sync/test_base.py
@@ -84,7 +84,7 @@ class SyncerSendPromptTests(unittest.IsolatedAsyncioTestCase):
method.assert_called_once_with(constants.Channels.dev_core)
- async def test_send_prompt_returns_None_if_channel_fetch_fails(self):
+ async def test_send_prompt_returns_none_if_channel_fetch_fails(self):
"""None should be returned if there's an HTTPException when fetching the channel."""
self.bot.get_channel.return_value = None
self.bot.fetch_channel.side_effect = discord.HTTPException(mock.MagicMock(), "test error!")
diff --git a/tests/bot/cogs/test_snekbox.py b/tests/bot/cogs/test_snekbox.py
index 9cd7f0154..fd9468829 100644
--- a/tests/bot/cogs/test_snekbox.py
+++ b/tests/bot/cogs/test_snekbox.py
@@ -89,15 +89,15 @@ class SnekboxTests(unittest.IsolatedAsyncioTestCase):
self.assertEqual(actual, expected)
@patch('bot.cogs.snekbox.Signals', side_effect=ValueError)
- def test_get_results_message_invalid_signal(self, mock_Signals: Mock):
+ def test_get_results_message_invalid_signal(self, mock_signals: Mock):
self.assertEqual(
self.cog.get_results_message({'stdout': '', 'returncode': 127}),
('Your eval job has completed with return code 127', '')
)
@patch('bot.cogs.snekbox.Signals')
- def test_get_results_message_valid_signal(self, mock_Signals: Mock):
- mock_Signals.return_value.name = 'SIGTEST'
+ def test_get_results_message_valid_signal(self, mock_signals: Mock):
+ mock_signals.return_value.name = 'SIGTEST'
self.assertEqual(
self.cog.get_results_message({'stdout': '', 'returncode': 127}),
('Your eval job has completed with return code 127 (SIGTEST)', '')
diff --git a/tests/bot/test_utils.py b/tests/bot/test_utils.py
deleted file mode 100644
index d7bcc3ba6..000000000
--- a/tests/bot/test_utils.py
+++ /dev/null
@@ -1,37 +0,0 @@
-import unittest
-
-from bot import utils
-
-
-class CaseInsensitiveDictTests(unittest.TestCase):
- """Tests for the `CaseInsensitiveDict` container."""
-
- def test_case_insensitive_key_access(self):
- """Tests case insensitive key access and storage."""
- instance = utils.CaseInsensitiveDict()
-
- key = 'LEMON'
- value = 'trees'
-
- instance[key] = value
- self.assertIn(key, instance)
- self.assertEqual(instance.get(key), value)
- self.assertEqual(instance.get(key.casefold()), value)
- self.assertEqual(instance.pop(key.casefold()), value)
- self.assertNotIn(key, instance)
- self.assertNotIn(key.casefold(), instance)
-
- instance.setdefault(key, value)
- del instance[key]
- self.assertNotIn(key, instance)
-
- def test_initialization_from_kwargs(self):
- """Tests creating the dictionary from keyword arguments."""
- instance = utils.CaseInsensitiveDict({'FOO': 'bar'})
- self.assertEqual(instance['foo'], 'bar')
-
- def test_update_from_other_mapping(self):
- """Tests updating the dictionary from another mapping."""
- instance = utils.CaseInsensitiveDict()
- instance.update({'FOO': 'bar'})
- self.assertEqual(instance['foo'], 'bar')