Repository: hikari-py/hikari
Pull request #1023 (2022-02-17 16:00:07)

Properly close when not fully started
### Summary
Properly close when not started correctly.

Cleans up errors with "Unclosed client session" and anything else the event loop might want to throw
![image](https://user-images.githubusercontent.com/29100934/154519962-4d08d797-e1b9-42e8-8e2c-370dc6de9e09.png)


### Checklist
<!-- Make sure to tick all the following boxes by putting an `x` in between (like this `[x]`) -->
- [x] I have run `nox` and all the pipelines have passed.
- [x] I have made unittests according to the code I have added/modified/deleted.

### Related issues
<!--
To mention an issue use `#issue-id` and to mention a merge request use `!merge-request-id`
To close/fix an issue use `Close #issue-id` or `Fix #issue-id` (depending on the merge request)
-->



Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index e6faf5e6..130ddf50 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -7,7 +7,7 @@ on:
 jobs:
   test:
     strategy:
-      # Allows for matrix sub-jobs to fail without canceling the rest
+      # Allows for matrix sub-jobs to fail without cancelling the rest
       fail-fast: false
       matrix:
         os: [ubuntu-latest, macos-latest, windows-latest]
@@ -17,10 +17,10 @@ jobs:
 
     steps:
       - name: Checkout repo
-        uses: actions/checkout@v2.4.0
+        uses: actions/checkout@v3
 
       - name: Setup python
-        uses: actions/setup-python@v2.3.2
+        uses: actions/setup-python@v3
         with:
           python-version: ${{ matrix.python-version }}
 
@@ -57,10 +57,10 @@ jobs:
 
     steps:
       - name: Checkout repo
-        uses: actions/checkout@v2.4.0
+        uses: actions/checkout@v3
 
       - name: Setup python
-        uses: actions/setup-python@v2.3.2
+        uses: actions/setup-python@v3
         with:
           python-version: 3.8
 
@@ -87,10 +87,10 @@ jobs:
 
     steps:
       - name: Checkout repo
-        uses: actions/checkout@v2.4.0
+        uses: actions/checkout@v3
 
       - name: Setup python
-        uses: actions/setup-python@v2.3.2
+        uses: actions/setup-python@v3
         with:
           python-version: 3.8
 
@@ -107,17 +107,17 @@ jobs:
       - name: Linting
         if: always()
         run: |
-          nox -s safety mypy verify-types flake8 codespell check-trailing-whitespaces
+          nox -s safety mypy verify-types flake8 slotscheck codespell check-trailing-whitespaces
 
   twemoji:
     runs-on: ubuntu-latest
 
     steps:
       - name: Checkout repo
-        uses: actions/checkout@v2.4.0
+        uses: actions/checkout@v3
 
       - name: Setup python
-        uses: actions/setup-python@v2.3.2
+        uses: actions/setup-python@v3
         with:
           python-version: 3.8
 
@@ -131,15 +131,15 @@ jobs:
 
     steps:
       - name: Checkout repo
-        uses: actions/checkout@v2.4.0
+        uses: actions/checkout@v3
 
       - name: Setup python
-        uses: actions/setup-python@v2.3.2
+        uses: actions/setup-python@v3
         with:
           python-version: 3.8
 
       - name: Setup node
-        uses: actions/setup-node@v2.5.1
+        uses: actions/setup-node@v3
 
       - name: Build pages
         run: |
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index f20fad6e..5aece327 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -15,7 +15,7 @@ jobs:
 
     steps:
     - name: Checkout repository
-      uses: actions/checkout@v2.4.0
+      uses: actions/checkout@v3
 
     - name: Initialize CodeQL
       uses: github/codeql-action/init@v1
diff --git a/.github/workflows/fragments-check.yml b/.github/workflows/fragments-check.yml
index a23eb0f8..5f40c08c 100644
--- a/.github/workflows/fragments-check.yml
+++ b/.github/workflows/fragments-check.yml
@@ -13,14 +13,14 @@ jobs:
 
     steps:
       - name: Checkout repo
-        uses: actions/checkout@v2.4.0
+        uses: actions/checkout@v3
         with:
           # `towncrier check` runs `git diff --name-only origin/main...`, which
           # needs a non-shallow clone.
           fetch-depth: 0
 
       - name: Setup python
-        uses: actions/setup-python@v2.3.2
+        uses: actions/setup-python@v3
         with:
           python-version: 3.8
 
diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml
index 792cc43f..91b1dfb0 100644
--- a/.github/workflows/prepare-release.yml
+++ b/.github/workflows/prepare-release.yml
@@ -8,15 +8,15 @@ on:
         required: true
 
 jobs:
-  prepare-relese:
+  prepare-release:
     runs-on: ubuntu-latest
 
     steps:
       - name: Checkout repo
-        uses: actions/checkout@v2.4.0
+        uses: actions/checkout@v3
 
       - name: Setup python
-        uses: actions/setup-python@v2.3.2
+        uses: actions/setup-python@v3
         with:
           python-version: 3.8
 
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 21456af2..73f30e0a 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -10,12 +10,12 @@ jobs:
 
     steps:
       - name: Checkout repo
-        uses: actions/checkout@v2.4.0
+        uses: actions/checkout@v3
         with:
           token: ${{ secrets.PAT_TOKEN }}
 
       - name: Setup python
-        uses: actions/setup-python@v2.3.2
+        uses: actions/setup-python@v3
         with:
           python-version: 3.8
 
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 39bc2380..b62581b7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -77,7 +77,7 @@ Bugfixes
 - Fix `PartialSticker.image_url` not passing the hash as a string ([#930](https://github.com/hikari-py/hikari/issues/930))
 - Fixed the url being generated for role icons to not erroneously insert ".png" before the file extension ([#931](https://github.com/hikari-py/hikari/issues/931))
 - Fix some bugs in message deserialization
-    - Remove case for setting `member` and `referece_message` to `undefined.Undefined` in full message deserialization
+    - Remove case for setting `member` and `reference_message` to `undefined.Undefined` in full message deserialization
     - Don't set `message.member` to `undefined.UNDEFINED` on partial message deserialization if message was sent by a webhook ([#933](https://github.com/hikari-py/hikari/issues/933))
 
 
diff --git a/README.md b/README.md
index 48576317..88ada165 100644
--- a/README.md
+++ b/README.md
@@ -20,6 +20,18 @@ development.
 
 Python 3.8, 3.9 and 3.10 are currently supported.
 
+## Installation
+
+Install Hikari from PyPI with the following command:
+
+```bash
+python -m pip install -U hikari
+# Windows users may need to run this instead...
+py -3 -m pip install -U hikari
+```
+
+----
+
 ## Bots
 
 ```py
@@ -116,18 +128,6 @@ async def print_my_user(token):
 asyncio.run(print_my_user("user token here"))
 ```
 
-----
-
-## Installation
-
-Install Hikari from PyPI with the following command:
-
-```bash
-python -m pip install -U hikari
-# Windows users may need to run this instead...
-py -3 -m pip install -U hikari
-```
-
 ---
 
 ## Optional Features
diff --git a/changes/1022.feature.md b/changes/1022.feature.md
new file mode 100644
index 00000000..4c98a0de
--- /dev/null
+++ b/changes/1022.feature.md
@@ -0,0 +1,1 @@
+Custom substitutions can now be used in `hikari.internal.ux.print_banner`.
diff --git a/changes/1023.bugfix.md b/changes/1023.bugfix.md
new file mode 100644
index 00000000..64d6d3e4
--- /dev/null
+++ b/changes/1023.bugfix.md
@@ -0,0 +1,1 @@
+Properly close `GatewayBot` when not fully started.
diff --git a/changes/1025.feature.md b/changes/1025.feature.md
new file mode 100644
index 00000000..e3d34eca
--- /dev/null
+++ b/changes/1025.feature.md
@@ -0,0 +1,1 @@
+`get_guild()` is now available on `hikari.Member`.
diff --git a/changes/1041.feature.md b/changes/1041.feature.md
new file mode 100644
index 00000000..ee23f87c
--- /dev/null
+++ b/changes/1041.feature.md
@@ -0,0 +1,1 @@
+The notorious "failed to communicate with server" log message is now a warning rather than an error.
diff --git a/changes/1042.feature.md b/changes/1042.feature.md
new file mode 100644
index 00000000..3a71eb71
--- /dev/null
+++ b/changes/1042.feature.md
@@ -0,0 +1,1 @@
+`hikari.applications`, `hikari.files`, `hikari.snowflakes` and `hikari.undefined` are now all explicitly exported by `hikari.__init__`, allowing pyright to see them without a direct import.
diff --git a/changes/1046.bugfix.md b/changes/1046.bugfix.md
new file mode 100644
index 00000000..e4775646
--- /dev/null
+++ b/changes/1046.bugfix.md
@@ -0,0 +1,3 @@
+The async context manager returned by `File.stream` now errors on enter if the target file doesn't exist to improve error handling when a file that doesn't exist is sent as an attachment.
+
+The multiprocessing file reader strategy now expands user relative (`~`) links (like the threaded strategy).
diff --git a/changes/796.feature.md b/changes/796.feature.md
new file mode 100644
index 00000000..6e79c534
--- /dev/null
+++ b/changes/796.feature.md
@@ -0,0 +1,3 @@
+Added a `total_length` function to `hikari.embeds.Embed`
+- Takes into account the character length of the embed's title, description, fields (all field names and values), footer, and author combined.
+- Useful for determining if the embed exceeds Discord's 6000 character limit.
diff --git a/changes/841.bugfix.md b/changes/841.bugfix.md
new file mode 100644
index 00000000..3bc02402
--- /dev/null
+++ b/changes/841.bugfix.md
@@ -0,0 +1,1 @@
+Fix bucket lock not being released on errors while being acquired, which locked the bucket infinitely
diff --git a/dev-requirements.txt b/dev-requirements.txt
index 832bded0..b7781f3d 100644
--- a/dev-requirements.txt
+++ b/dev-requirements.txt
@@ -7,12 +7,12 @@ mock==4.0.3
 
 # Py.test stuff.
 pytest==7.0.1
-pytest-asyncio==0.18.1
+pytest-asyncio==0.18.2
 pytest-cov==3.0.0
 pytest-randomly==3.11.0
 
 # Coverage testing.
-coverage[toml]==6.3.1
+coverage[toml]==6.3.2
 
 # Other stuff
 async-timeout==4.0.2  # Used for timeouts in some test cases.
@@ -29,7 +29,7 @@ sphobjinv==2.2.1
 #################
 
 mypy==0.931
-pyright==0.0.13
+pyright==1.1.226
 
 #######################
 # DEPENDENCY CHECKING #
@@ -37,13 +37,19 @@ pyright==0.0.13
 
 safety~=1.10.3
 
-#############
+##############
 # FORMATTING #
-#############
+##############
 
 black==22.1.0
 isort==5.10.1
 
+###########
+# Linting #
+###########
+
+slotscheck==0.14.0
+
 ##################
 # SPELL CHECKING #
 ##################
diff --git a/examples/slash.py b/examples/slash.py
new file mode 100644
index 00000000..af6da446
--- /dev/null
+++ b/examples/slash.py
@@ -0,0 +1,72 @@
+# -*- coding: utf-8 -*-
+# Hikari Examples - A collection of examples for Hikari.
+#
+# To the extent possible under law, the author(s) have dedicated all copyright
+# and related and neighboring rights to this software to the public domain worldwide.
+# This software is distributed without any warranty.
+#
+# You should have received a copy of the CC0 Public Domain Dedication along with this software.
+# If not, see <https://creativecommons.org/publicdomain/zero/1.0/>.
+"""A simple bot with some simple commands."""
+
+import os
+
+import hikari
+
+bot = hikari.GatewayBot(token=os.environ["BOT_TOKEN"])
+
+# Set this to the guild to create the slash commands in.
+# To create global commands, set to `hikari.UNDEFINED`.
+#
+# It is important to note that global commands can take up to an hour to show in the client
+# and should only be used when "publishing" commands after they are built. For testing
+# use guild based commands as they are instant.
+COMMAND_GUILD_ID = 0
+
+
+@bot.listen()
+async def register_commands(event: hikari.StartingEvent) -> None:
+    """Register ping and info commands."""
+    application = await bot.rest.fetch_application()
+
+    commands = [
+        bot.rest.slash_command_builder("ping", "Get the bot's latency."),
+        bot.rest.slash_command_builder("info", "Learn something about the bot."),
+        bot.rest.slash_command_builder("ephemeral", "Send a very secret message."),
+    ]
+
+    await bot.rest.set_application_commands(
+        application=application.id,
+        commands=commands,
+        guild=COMMAND_GUILD_ID,
+    )
+
+
+@bot.listen()
+async def handle_interactions(event: hikari.InteractionCreateEvent) -> None:
+    """Listen for slash commands being executed."""
+    if not isinstance(event.interaction, hikari.CommandInteraction):
+        # only listen to command interactions, no others!
+        return
+
+    if event.interaction.command_name == "ping":
+        await event.interaction.create_initial_response(
+            hikari.ResponseType.MESSAGE_CREATE,
+            f"Pong! {bot.heartbeat_latency * 1_000:.0f}ms",
+        )
+
+    elif event.interaction.command_name == "info":
+        await event.interaction.create_initial_response(
+            hikari.ResponseType.MESSAGE_CREATE,
+            "Hello, this is an example bot written in hikari!",
+        )
+
+    elif event.interaction.command_name == "ephemeral":
+        await event.interaction.create_initial_response(
+            hikari.ResponseType.MESSAGE_CREATE,
+            "Only you can see this, keep it a secret :)",
+            flags=hikari.MessageFlag.EPHEMERAL,
+        )
+
+
+bot.run()
diff --git a/flake8-requirements.txt b/flake8-requirements.txt
index dc5f44e6..140ce957 100644
--- a/flake8-requirements.txt
+++ b/flake8-requirements.txt
@@ -3,8 +3,9 @@ flake8==4.0.1
 # Plugins
 # Ref: https://github.com/DmytroLitvinov/awesome-flake8-extensions
 
+bandit==1.7.2                           # temporariry freeze to bandit due to incompatibility with flake8-bandit
 flake8-bandit~=2.1.2                    # runs bandit
-flake8-black==0.2.4                     # runs black
+flake8-black==0.3.2                     # runs black
 flake8-broken-line==0.4.0               # forbey "\" linebreaks
 flake8-builtins==1.5.3                  # builtin shadowing checks
 flake8-coding==1.3.2                    # coding magic-comment detection
@@ -13,7 +14,7 @@ flake8-deprecated==1.3                  # deprecated call checks
 flake8-docstrings==1.6.0                # pydocstyle support
 flake8-executable==2.1.1                # shebangs
 flake8-fixme==1.1.1                     # "fix me" counter
-flake8-functions==0.0.6                 # function linting
+flake8-functions==0.0.7                 # function linting
 flake8-html==0.4.1                      # html output
 flake8-if-statements==0.1.0             # condition linting
 flake8-isort==4.1.1                     # runs isort
@@ -23,4 +24,4 @@ flake8-print==4.0.0                     # complain about print statements in cod
 flake8-printf-formatting==1.1.2         # forbey printf-style python2 string formatting
 flake8-pytest-style==1.6.0              # pytest checks
 flake8-raise==0.0.5                     # exception raising linting
-flake8-use-fstring==1.3               # format string checking
+flake8-use-fstring==1.3                 # format string checking
diff --git a/hikari/__init__.py b/hikari/__init__.py
index f611993b..f678b383 100644
--- a/hikari/__init__.py
+++ b/hikari/__init__.py
@@ -31,7 +31,11 @@
 import os as _os
 
 from hikari import api
+from hikari import applications
+from hikari import files
 from hikari import impl
+from hikari import snowflakes
+from hikari import undefined
 from hikari._about import __author__
 from hikari._about import __ci__
 from hikari._about import __copyright__
diff --git a/hikari/__init__.pyi b/hikari/__init__.pyi
index 6d5b36df..27ec425b 100644
--- a/hikari/__init__.pyi
+++ b/hikari/__init__.pyi
@@ -4,7 +4,11 @@
 from typing import Any
 
 from hikari import api as api
+from hikari import applications as applications
+from hikari import files as files
 from hikari import impl as impl
+from hikari import snowflakes as snowflakes
+from hikari import undefined as undefined
 from hikari._about import __author__ as __author__
 from hikari._about import __ci__ as __ci__
 from hikari._about import __copyright__ as __copyright__
diff --git a/hikari/api/interaction_server.py b/hikari/api/interaction_server.py
index 2898b9c4..4d59902b 100644
--- a/hikari/api/interaction_server.py
+++ b/hikari/api/interaction_server.py
@@ -46,7 +46,7 @@
 """Type hint of a Interaction server's listener callback.
 
 This should be an async callback which takes in one positional argument which
-subclases `hikari.interactions.base_interactions.PartialInteraction` and may return an
+subclasses `hikari.interactions.base_interactions.PartialInteraction` and may return an
 instance of the relevant `hikari.api.special_endpoints.InteractionResponseBuilder`
 subclass for the provided interaction type which will instruct the server on how
 to respond.
diff --git a/hikari/api/rest.py b/hikari/api/rest.py
index f750d049..0f76717c 100644
--- a/hikari/api/rest.py
+++ b/hikari/api/rest.py
@@ -1988,7 +1988,7 @@ async def fetch_webhook(
         Other Parameters
         ----------------
         token : hikari.undefined.UndefinedOr[builtins.str]
-            If provided, the webhoook token that will be used to fetch
+            If provided, the webhook token that will be used to fetch
             the webhook instead of the token the client was initialized with.
 
         Returns
@@ -2125,7 +2125,7 @@ async def edit_webhook(
         Other Parameters
         ----------------
         token : hikari.undefined.UndefinedOr[builtins.str]
-            If provided, the webhoook token that will be used to edit
+            If provided, the webhook token that will be used to edit
             the webhook instead of the token the client was initialized with.
         name : hikari.undefined.UndefinedOr[builtins.str]
             If provided, the new webhook name.
@@ -2185,7 +2185,7 @@ async def delete_webhook(
         Other Parameters
         ----------------
         token : hikari.undefined.UndefinedOr[builtins.str]
-            If provided, the webhoook token that will be used to delete
+            If provided, the webhook token that will be used to delete
             the webhook instead of the token the client was initialized with.
 
         Raises
@@ -2196,7 +2196,7 @@ async def delete_webhook(
         hikari.errors.UnauthorizedError
             If you are unauthorized to make the request (invalid/missing token).
         hikari.errors.NotFoundError
-            If the webhoook is not found.
+            If the webhook is not found.
         hikari.errors.RateLimitTooLongError
             Raised in the event that a rate limit occurs that is
             longer than `max_rate_limit` when making a request.
@@ -2885,7 +2885,7 @@ def fetch_my_guilds(
         Other Parameters
         ----------------
         newest_first : builtins.bool
-            Whether to fetch the newest first or the olders first.
+            Whether to fetch the newest first or the oldest first.
             Defaults to `builtins.False`.
         start_at : hikari.undefined.UndefinedOr[hikari.snowflakes.SearchableSnowflakeishOr[hikari.guilds.PartialGuild]]
             If provided, will start at this snowflake. If you provide
@@ -3413,7 +3413,7 @@ def fetch_audit_log(
         user: undefined.UndefinedOr[snowflakes.SnowflakeishOr[users.PartialUser]] = undefined.UNDEFINED,
         event_type: undefined.UndefinedOr[typing.Union[audit_logs.AuditLogEventType, int]] = undefined.UNDEFINED,
     ) -> iterators.LazyIterator[audit_logs.AuditLog]:
-        """Fetch the guild's audit log.
+        """Fetch pages of the guild's audit log.
 
         Parameters
         ----------
@@ -7234,7 +7234,7 @@ async def fetch_application_command_permissions(
         guild : hikari.undefined.UndefinedOr[hikari.snowflakes.SnowflakeishOr[hikari.guilds.PartialGuild]]
             Object or ID of the guild to fetch the command permissions for.
         command: hikari.snowflakes.SnowflakeishOr[hikari.commands.PartialCommand]
-            Objecr or ID of the command to fetch the command permissions for.
+            Object or ID of the command to fetch the command permissions for.
 
         Returns
         -------
diff --git a/hikari/api/shard.py b/hikari/api/shard.py
index a3cfc4f6..7f4633c1 100644
--- a/hikari/api/shard.py
+++ b/hikari/api/shard.py
@@ -141,11 +141,11 @@ async def get_user_id(self) -> snowflakes.Snowflake:
 
     @abc.abstractmethod
     async def close(self) -> None:
-        """Close the websocket if it is connected, otherwise do nothing."""
+        """Close the websocket if it is connected."""
 
     @abc.abstractmethod
     async def join(self) -> None:
-        """Wait indefinitely until the websocket closes permanently.
+        """Wait indefinitely until the websocket closes.
 
         This can be placed in a task and cancelled without affecting the
         websocket runtime itself.
diff --git a/hikari/api/special_endpoints.py b/hikari/api/special_endpoints.py
index cd3230d8..3777e52c 100644
--- a/hikari/api/special_endpoints.py
+++ b/hikari/api/special_endpoints.py
@@ -552,6 +552,8 @@ def add_stage_channel(
 class InteractionResponseBuilder(abc.ABC):
     """Base class for all interaction response builders used in the interaction server."""
 
+    __slots__: typing.Sequence[str] = ()
+
     @property
     @abc.abstractmethod
     def type(self) -> typing.Union[int, base_interactions.ResponseType]:
@@ -1018,6 +1020,8 @@ def build(self, entity_factory: entity_factory_.EntityFactory, /) -> data_bindin
 class SlashCommandBuilder(CommandBuilder):
     """SlashCommandBuilder."""
 
+    __slots__: typing.Sequence[str] = ()
+
     @property
     @abc.abstractmethod
     def description(self) -> str:
@@ -1065,6 +1069,8 @@ def add_option(self: _T, option: commands.CommandOption) -> _T:
 class ContextMenuCommandBuilder(CommandBuilder):
     """ContextMenuCommandBuilder."""
 
+    __slots__: typing.Sequence[str] = ()
+
 
 class ComponentBuilder(abc.ABC):
     """Base class for all component builder classes."""
@@ -1205,6 +1211,8 @@ def add_to_container(self) -> _ContainerT:
 class LinkButtonBuilder(ButtonBuilder[_ContainerT], abc.ABC):
     """Builder interface for link buttons."""
 
+    __slots__: typing.Sequence[str] = ()
+
     @property
     @abc.abstractmethod
     def url(self) -> str:
@@ -1220,6 +1228,8 @@ def url(self) -> str:
 class InteractiveButtonBuilder(ButtonBuilder[_ContainerT], abc.ABC):
     """Builder interface for interactive buttons."""
 
+    __slots__: typing.Sequence[str] = ()
+
     @property
     @abc.abstractmethod
     def custom_id(self) -> str:
diff --git a/hikari/audit_logs.py b/hikari/audit_logs.py
index 45f5a211..7cb0723f 100644
--- a/hikari/audit_logs.py
+++ b/hikari/audit_logs.py
@@ -487,7 +487,7 @@ async def fetch_user(self) -> typing.Optional[users_.User]:
 @attr_extensions.with_copy
 @attr.define(hash=False, kw_only=True, repr=False, weakref_slot=False)
 class AuditLog(typing.Sequence[AuditLogEntry]):
-    """Represents a guilds audit log."""
+    """Represents a guilds audit log's page."""
 
     entries: typing.Mapping[snowflakes.Snowflake, AuditLogEntry] = attr.field(repr=False)
     """A mapping of snowflake IDs to the audit log's entries."""
diff --git a/hikari/channels.py b/hikari/channels.py
index fc992702..aa34ece2 100644
--- a/hikari/channels.py
+++ b/hikari/channels.py
@@ -1080,9 +1080,7 @@ async def edit_overwrite(
 
     async def remove_overwrite(
         self,
-        target: snowflakes.SnowflakeishOr[
-            typing.Union[PermissionOverwrite, guilds.PartialRole, users.PartialUser, snowflakes.Snowflakeish]
-        ],
+        target: typing.Union[PermissionOverwrite, guilds.PartialRole, users.PartialUser, snowflakes.Snowflakeish],
     ) -> None:
         """Delete a custom permission for an entity in a given guild channel.
 
diff --git a/hikari/embeds.py b/hikari/embeds.py
index 388bd18e..0b385cc8 100644
--- a/hikari/embeds.py
+++ b/hikari/embeds.py
@@ -991,3 +991,26 @@ def __eq__(self, other: typing.Any) -> bool:
             else:
                 return True
         return False
+
+    def total_length(self) -> int:
+        """Get the total character count of the embed.
+
+        Returns
+        -------
+        builtins.int
+            The total character count of this embed, including title, description,
+            fields, footer, and author combined.
+        """
+        total = len(self._title or "") + len(self._description or "")
+
+        if self._fields:
+            for field in self._fields:
+                total += len(field.name) + len(field.value)
+
+        if self._footer and self._footer.text:
+            total += len(self._footer.text)
+
+        if self._author and self._author.name:
+            total += len(self._author.name)
+
+        return total
diff --git a/hikari/errors.py b/hikari/errors.py
index 7e3eed10..642ffd80 100644
--- a/hikari/errors.py
+++ b/hikari/errors.py
@@ -66,8 +66,9 @@
     from hikari.internal import routes
 
 
+# The standard exceptions are all unsloted so slotting here would be a waste of time.
 @attr_extensions.with_copy
-@attr.define(auto_exc=True, repr=False, init=False, weakref_slot=False)
+@attr.define(auto_exc=True, repr=False, init=False, slots=False)
 class HikariError(RuntimeError):
     """Base for an error raised by this API.
 
@@ -78,8 +79,9 @@ class HikariError(RuntimeError):
     """
 
 
+# The standard warnings are all unsloted so slotting here would be a waste of time.
 @attr_extensions.with_copy
-@attr.define(auto_exc=True, repr=False, init=False, weakref_slot=False)
+@attr.define(auto_exc=True, repr=False, init=False, slots=False)
 class HikariWarning(RuntimeWarning):
     """Base for a warning raised by this API.
 
@@ -90,7 +92,7 @@ class HikariWarning(RuntimeWarning):
     """
 
 
-@attr.define(auto_exc=True, repr=False, weakref_slot=False)
+@attr.define(auto_exc=True, repr=False, slots=False)
 class HikariInterrupt(KeyboardInterrupt, HikariError):
     """Exception raised when a kill signal is handled internally."""
 
@@ -101,7 +103,7 @@ class HikariInterrupt(KeyboardInterrupt, HikariError):
     """The signal name that was raised."""
 
 
-@attr.define(auto_exc=True, repr=False, weakref_slot=False)
+@attr.define(auto_exc=True, repr=False, slots=False)
 class ComponentStateConflictError(HikariError):
     """Exception thrown when an action cannot be executed in the component's current state.
 
@@ -116,7 +118,7 @@ def __str__(self) -> str:
         return self.reason
 
 
-@attr.define(auto_exc=True, repr=False, weakref_slot=False)
+@attr.define(auto_exc=True, repr=False, slots=False)
 class UnrecognisedEntityError(HikariError):
     """An exception thrown when an unrecognised entity is found."""
 
@@ -127,7 +129,7 @@ def __str__(self) -> str:
         return self.reason
 
 
-@attr.define(auto_exc=True, repr=False, weakref_slot=False)
+@attr.define(auto_exc=True, repr=False, slots=False)
 class GatewayError(HikariError):
     """A base exception type for anything that can be thrown by the Gateway."""
 
@@ -171,7 +173,7 @@ def is_standard(self) -> bool:
         return (self.value // 1000) == 1
 
 
-@attr.define(auto_exc=True, repr=False, weakref_slot=False)
+@attr.define(auto_exc=True, repr=False, slots=False)
 class GatewayConnectionError(GatewayError):
     """An exception thrown if a connection issue occurs."""
 
@@ -179,7 +181,7 @@ def __str__(self) -> str:
         return f"Failed to connect to server: {self.reason!r}"
 
 
-@attr.define(auto_exc=True, repr=False, weakref_slot=False)
+@attr.define(auto_exc=True, repr=False, slots=False)
 class GatewayServerClosedConnectionError(GatewayError):
     """An exception raised when the server closes the connection."""
 
@@ -214,7 +216,7 @@ def __str__(self) -> str:
         return f"Server closed connection with code {self.code} ({self.reason})"
 
 
-@attr.define(auto_exc=True, repr=False, weakref_slot=False)
+@attr.define(auto_exc=True, repr=False, slots=False)
 class HTTPError(HikariError):
     """Base exception raised if an HTTP error occurs while making a request."""
 
@@ -222,7 +224,7 @@ class HTTPError(HikariError):
     """The error message."""
 
 
-@attr.define(auto_exc=True, repr=False, weakref_slot=False)
+@attr.define(auto_exc=True, repr=False, slots=False)
 class HTTPResponseError(HTTPError):
     """Base exception for an erroneous HTTP response."""
 
@@ -266,7 +268,7 @@ def __str__(self) -> str:
         return f"{name_value}:{code_str} '{body[:200]}{'...' if chomped else ''}' for {self.url}"
 
 
-@attr.define(auto_exc=True, repr=False, weakref_slot=False)
+@attr.define(auto_exc=True, repr=False, slots=False)
 class ClientHTTPResponseError(HTTPResponseError):
     """Base exception for an erroneous HTTP response that is a client error.
 
@@ -275,7 +277,7 @@ class ClientHTTPResponseError(HTTPResponseError):
     """
 
 
-@attr.define(auto_exc=True, repr=False, weakref_slot=False)
+@attr.define(auto_exc=True, repr=False, slots=False)
 class BadRequestError(ClientHTTPResponseError):
     """Raised when you send an invalid request somehow."""
 
@@ -304,7 +306,7 @@ def __str__(self) -> str:
         return value
 
 
-@attr.define(auto_exc=True, repr=False, weakref_slot=False)
+@attr.define(auto_exc=True, repr=False, slots=False)
 class UnauthorizedError(ClientHTTPResponseError):
     """Raised when you are not authorized to access a specific resource."""
 
@@ -312,7 +314,7 @@ class UnauthorizedError(ClientHTTPResponseError):
     """The HTTP status code for the response."""
 
 
-@attr.define(auto_exc=True, repr=False, weakref_slot=False)
+@attr.define(auto_exc=True, repr=False, slots=False)
 class ForbiddenError(ClientHTTPResponseError):
     """Raised when you are not allowed to access a specific resource.
 
@@ -325,7 +327,7 @@ class ForbiddenError(ClientHTTPResponseError):
     """The HTTP status code for the response."""
 
 
-@attr.define(auto_exc=True, repr=False, weakref_slot=False)
+@attr.define(auto_exc=True, repr=False, slots=False)
 class NotFoundError(ClientHTTPResponseError):
     """Raised when something is not found."""
 
@@ -333,7 +335,7 @@ class NotFoundError(ClientHTTPResponseError):
     """The HTTP status code for the response."""
 
 
-@attr.define(auto_exc=True, kw_only=True, repr=False, weakref_slot=False)
+@attr.define(auto_exc=True, kw_only=True, repr=False, slots=False)
 class RateLimitedError(ClientHTTPResponseError):
     """Raised when a non-global rate limit that cannot be handled occurs.
 
@@ -367,7 +369,7 @@ def _(self) -> str:
         return f"You are being rate-limited for {self.retry_after:,} seconds on route {self.route}. Please slow down!"
 
 
-@attr.define(auto_exc=True, kw_only=True, repr=False, weakref_slot=False)
+@attr.define(auto_exc=True, kw_only=True, repr=False, slots=False)
 class RateLimitTooLongError(HTTPError):
     """Internal error raised if the wait for a rate limit is too long.
 
@@ -428,7 +430,7 @@ def __str__(self) -> str:
         return self.message
 
 
-@attr.define(auto_exc=True, repr=False, weakref_slot=False)
+@attr.define(auto_exc=True, repr=False, slots=False)
 class InternalServerError(HTTPResponseError):
     """Base exception for an erroneous HTTP response that is a server error.
 
@@ -437,7 +439,7 @@ class InternalServerError(HTTPResponseError):
     """
 
 
-@attr.define(auto_exc=True, repr=False, init=False, weakref_slot=False)
+@attr.define(auto_exc=True, repr=False, init=False, slots=False)
 class MissingIntentWarning(HikariWarning):
     """Warning raised when subscribing to an event that cannot be fired.
 
@@ -445,7 +447,7 @@ class MissingIntentWarning(HikariWarning):
     """
 
 
-@attr.define(auto_exc=True, repr=False, weakref_slot=False)
+@attr.define(auto_exc=True, repr=False, slots=False)
 class BulkDeleteError(HikariError):
     """Exception raised when a bulk delete fails midway through a call.
 
@@ -478,12 +480,12 @@ def __str__(self) -> str:
         return f"Error encountered when bulk deleting messages ({deleted}/{total} messages deleted)"
 
 
-@attr.define(auto_exc=True, repr=False, init=False, weakref_slot=False)
+@attr.define(auto_exc=True, repr=False, init=False, slots=False)
 class VoiceError(HikariError):
     """Error raised when a problem occurs with the voice subsystem."""
 
 
-@attr.define(auto_exc=True, repr=False, weakref_slot=False)
+@attr.define(auto_exc=True, repr=False, slots=False)
 class MissingIntentError(HikariError, ValueError):
     """Error raised when you try to perform an action without an intent.
 
diff --git a/hikari/files.py b/hikari/files.py
index 0a09008f..10b6dbac 100644
--- a/hikari/files.py
+++ b/hikari/files.py
@@ -53,6 +53,7 @@
 import mimetypes
 import os
 import pathlib
+import stat
 import typing
 import urllib.parse
 import urllib.request
@@ -394,7 +395,7 @@ def __exit__(self, exc_type: typing.Type[Exception], exc_val: Exception, exc_tb:
 
 @attr.define(weakref_slot=False)
 @typing.final
-class _NoOpAsyncReaderContextManagerImpl(typing.Generic[ReaderImplT], AsyncReaderContextManager[ReaderImplT]):
+class _NoOpAsyncReaderContextManagerImpl(AsyncReaderContextManager[ReaderImplT]):
     impl: ReaderImplT = attr.field()
 
     async def __aenter__(self) -> ReaderImplT:
@@ -498,6 +499,8 @@ def stream(
         -------
         AsyncReaderContextManager[AsyncReader]
             An async iterable of bytes to stream.
+
+            This will error on enter if the target resource doesn't exist.
         """
 
     def __str__(self) -> str:
@@ -768,6 +771,54 @@ class FileReader(AsyncReader, abc.ABC):
     """The path to the resource to read."""
 
 
+def _stat(path: pathlib.Path) -> os.stat_result:
+    # While paths will be implicitly resolved, we still need to explicitly
+    # call expanduser to deal with a ~ base.
+    try:
+        path = path.expanduser()
+    except RuntimeError:
+        pass  # A home directory couldn't be resolved, so we'll just use the path as-is.
+
+    return path.stat()
+
+
+@attr.define(weakref_slot=False)
+@typing.final
+class _FileAsyncReaderContextManagerImpl(AsyncReaderContextManager[FileReader]):
+    impl: FileReader = attr.field()
+
+    async def __aenter__(self) -> FileReader:
+        loop = asyncio.get_running_loop()
+
+        # Will raise FileNotFoundError if the file doesn't exist (unlike is_dir),
+        # which is what we want here.
+        file_stats = await loop.run_in_executor(self.impl.executor, _stat, self.impl.path)
+
+        if stat.S_ISDIR(file_stats.st_mode):
+            raise IsADirectoryError(self.impl.path)
+
+        return self.impl
+
+    async def __aexit__(
+        self,
+        exc_type: typing.Optional[typing.Type[BaseException]],
+        exc: typing.Optional[BaseException],
+        exc_tb: typing.Optional[types.TracebackType],
+    ) -> None:
+        pass
+
+
+def _open_file(path: pathlib.Path) -> typing.BinaryIO:
+    # While paths will be implicitly resolved, we still need to explicitly
+    # call expanduser to deal with a ~ base.
+    try:
+        path = path.expanduser()
+    except RuntimeError:
+        pass  # A home directory couldn't be resolved, so we'll just use the path as-is.
+
+    return path.open("rb")
+
+
 @attr.define(weakref_slot=False)
 class ThreadedFileReader(FileReader):
     """Asynchronous file reader that reads a resource from local storage.
@@ -780,40 +831,29 @@ class ThreadedFileReader(FileReader):
     async def __aiter__(self) -> typing.AsyncGenerator[typing.Any, bytes]:
         loop = asyncio.get_running_loop()
 
-        path = self.path
-        if isinstance(path, pathlib.Path):
-            path = await loop.run_in_executor(self.executor, self._expand, self.path)
-
-        fp = await loop.run_in_executor(self.executor, self._open, path)
+        fp = await loop.run_in_executor(self.executor, _open_file, self.path)
 
         try:
             while True:
-                chunk = await loop.run_in_executor(self.executor, self._read_chunk, fp, _MAGIC)
+                chunk = await loop.run_in_executor(self.executor, fp.read, _MAGIC)
                 yield chunk
                 if len(chunk) < _MAGIC:
                     break
 
         finally:
-            await loop.run_in_executor(self.executor, self._close, fp)
-
-    @staticmethod
-    def _expand(path: pathlib.Path) -> pathlib.Path:
-        # .expanduser is Platform dependent. Will expand stuff like ~ to /home/<user> on posix.
-        # .resolve will follow symlinks and what-have-we to translate stuff like `..` to proper paths.
-        return path.expanduser().resolve()
+            await loop.run_in_executor(self.executor, fp.close)
 
-    @staticmethod
-    @typing.final
-    def _read_chunk(fp: typing.IO[bytes], n: int = 10_000) -> bytes:
-        return fp.read(n)
 
-    @staticmethod
-    def _open(path: Pathish) -> typing.IO[bytes]:
-        return open(path, "rb")
+def _read_all(path: pathlib.Path) -> bytes:
+    # While paths will be implicitly resolved, we still need to explicitly
+    # call expanduser to deal with a ~ base.
+    try:
+        path = path.expanduser()
+    except RuntimeError:
+        pass  # A home directory couldn't be resolved, so we'll just use the path as-is.
 
-    @staticmethod
-    def _close(fp: typing.IO[bytes]) -> None:
-        fp.close()
+    with path.open("rb") as fp:
+        return fp.read()
 
 
 @attr.define(slots=False, weakref_slot=False)  # Do not slot (pickle)
@@ -827,7 +867,7 @@ class MultiprocessingFileReader(FileReader):
     """
 
     async def __aiter__(self) -> typing.AsyncGenerator[typing.Any, bytes]:
-        yield await asyncio.get_running_loop().run_in_executor(self.executor, self._read_all)
+        yield await asyncio.get_running_loop().run_in_executor(self.executor, _read_all, self.path)
 
     def __getstate__(self) -> typing.Dict[str, typing.Any]:
         return {"path": self.path, "filename": self.filename}
@@ -838,10 +878,6 @@ def __setstate__(self, state: typing.Dict[str, typing.Any]) -> None:
         self.executor: typing.Optional[concurrent.futures.Executor] = None
         self.mimetype: typing.Optional[str] = None
 
-    def _read_all(self) -> bytes:
-        with open(self.path, "rb") as fp:
-            return fp.read()
-
 
 class File(Resource[FileReader]):
     """A resource that exists on the local machine's storage to be uploaded.
@@ -921,7 +957,7 @@ def stream(
         # so this is safe enough to do.:
         is_threaded = executor is None or isinstance(executor, concurrent.futures.ThreadPoolExecutor)
         impl = ThreadedFileReader if is_threaded else MultiprocessingFileReader
-        return _NoOpAsyncReaderContextManagerImpl(impl(self.filename, None, executor, self.path))
+        return _FileAsyncReaderContextManagerImpl(impl(self.filename, None, executor, self.path))
 
 
 ########################################################################
diff --git a/hikari/guilds.py b/hikari/guilds.py
index 47a27f2c..b4728428 100644
--- a/hikari/guilds.py
+++ b/hikari/guilds.py
@@ -508,7 +508,7 @@ def mention(self) -> str:
     def communication_disabled_until(self) -> typing.Optional[datetime.datetime]:
         """Return when the timeout for this member ends.
 
-        Unlike `raw_communictation_disabled_until`, this will always be
+        Unlike `raw_communication_disabled_until`, this will always be
         `builtins.None` if the member is not currently timed out.
 
         !!! note
@@ -522,6 +522,19 @@ def communication_disabled_until(self) -> typing.Optional[datetime.datetime]:
             return self.raw_communication_disabled_until
         return None
 
+    def get_guild(self) -> typing.Optional[Guild]:
+        """Return the guild associated with this member.
+
+        Returns
+        -------
+        typing.Optional[hikari.guilds.Guild]
+            `builtins.None` if the the guild is not cached.
+        """
+        if not isinstance(self.user.app, traits.CacheAware):
+            return None
+
+        return self.user.app.cache.get_guild(self.guild_id)
+
     def get_presence(self) -> typing.Optional[presences_.MemberPresence]:
         """Get the cached presence for this member, if known.
 
@@ -3334,7 +3347,7 @@ async def fetch_public_updates_channel(self) -> typing.Optional[channels_.GuildT
         Returns
         -------
         typing.Optional[hikari.channels.GuildTextChannel]
-            The channel where discord sents relevant updates to moderators and admins.
+            The channel where discord sends relevant updates to moderators and admins.
 
         Raises
         ------
diff --git a/hikari/impl/bot.py b/hikari/impl/bot.py
index 67b1b3f7..81cb6879 100644
--- a/hikari/impl/bot.py
+++ b/hikari/impl/bot.py
@@ -439,7 +439,6 @@ def get_me(self) -> typing.Optional[users_.OwnUser]:
         return self._cache.get_me()
 
     async def close(self) -> None:
-        """Kill the application by shutting all components down."""
         self._check_if_alive()
         await self._close()
 
@@ -455,6 +454,7 @@ async def _close(self) -> None:
         self._closed_event = asyncio.Event()
         self._closing_event.set()
         self._closing_event = None
+        dispatch_events = self._is_alive
 
         loop = asyncio.get_running_loop()
 
@@ -472,7 +472,8 @@ async def handle(name: str, awaitable: typing.Awaitable[typing.Any]) -> None:
                     }
                 )
 
-        await self._event_manager.dispatch(self._event_factory.deserialize_stopping_event())
+        if dispatch_events:
+            await self._event_manager.dispatch(self._event_factory.deserialize_stopping_event())
 
         _LOGGER.log(ux.TRACE, "StoppingEvent dispatch completed, now beginning termination")
 
@@ -490,7 +491,9 @@ async def handle(name: str, awaitable: typing.Awaitable[typing.Any]) -> None:
         self._shards.clear()
         self._is_alive = False
 
-        await self._event_manager.dispatch(self._event_factory.deserialize_stopped_event())
+        if dispatch_events:
+            await self._event_manager.dispatch(self._event_factory.deserialize_stopped_event())
+
         self._closed_event.set()
         self._closed_event = None
 
@@ -572,7 +575,7 @@ async def on_everyone_mentioned(event):
         Listen: `hikari.impl.bot.GatewayBot.listen`
         Stream: `hikari.impl.bot.GatewayBot.stream`
         Subscribe: `hikari.impl.bot.GatewayBot.subscribe`
-        Unubscribe: `hikari.impl.bot.GatewayBot.unsubscribe`
+        Unsubscribe: `hikari.impl.bot.GatewayBot.unsubscribe`
         Wait_for: `hikari.impl.bot.GatewayBot.wait_for`
         """
         return self._event_manager.dispatch(event)
@@ -643,13 +646,18 @@ def listen(
         Dispatch: `hikari.impl.bot.GatewayBot.dispatch`
         Stream: `hikari.impl.bot.GatewayBot.stream`
         Subscribe: `hikari.impl.bot.GatewayBot.subscribe`
-        Unubscribe: `hikari.impl.bot.GatewayBot.unsubscribe`
+        Unsubscribe: `hikari.impl.bot.GatewayBot.unsubscribe`
         Wait_for: `hikari.impl.bot.GatewayBot.wait_for`
         """
         return self._event_manager.listen(event_type)
 
     @staticmethod
-    def print_banner(banner: typing.Optional[str], allow_color: bool, force_color: bool) -> None:
+    def print_banner(
+        banner: typing.Optional[str],
+        allow_color: bool,
+        force_color: bool,
+        extra_args: typing.Optional[typing.Dict[str, str]] = None,
+    ) -> None:
         """Print the banner.
 
         This allows library vendors to override this behaviour, or choose to
@@ -674,8 +682,16 @@ def print_banner(banner: typing.Optional[str], allow_color: bool, force_color: b
 
         !!! note
             `force_color` will always take precedence over `allow_color`.
+        extra_args : typing.Optional[typing.Dict[builtins.str, builtins.str]]
+            If provided, extra $-substitutions to use when printing the banner.
+            Default substitutions can not be overwritten.
+
+        Raises
+        ------
+        builtins.ValueError
+            If `extra_args` contains a default $-substitution.
         """
-        ux.print_banner(banner, allow_color, force_color)
+        ux.print_banner(banner, allow_color, force_color, extra_args=extra_args)
 
     def run(
         self,
@@ -722,7 +738,7 @@ def run(
         close_loop : builtins.bool
             Defaults to `builtins.True`. If `builtins.True`, then once the bot
             enters a state where all components have shut down permanently
-            during application shutdown, then all asyngens and background tasks
+            during application shutdown, then all asyncgens and background tasks
             will be destroyed, and the event loop will be shut down.
 
             This will wait until all `hikari`-owned `aiohttp` connectors have
@@ -968,9 +984,11 @@ async def start(
 
         _validate_activity(activity)
 
-        # Dispatch the update checker, the sharding requirements checker, and dispatch
-        # the starting event together to save a little time on startup.
         start_time = time.monotonic()
+        self._rest.start()
+        self._voice.start()
+        self._closing_event = asyncio.Event()
+        self._is_alive = True
 
         if check_for_updates:
             asyncio.create_task(
@@ -978,9 +996,8 @@ async def start(
                 name="check for package updates",
             )
 
-        self._rest.start()
-        await self._event_manager.dispatch(self._event_factory.deserialize_starting_event())
         requirements = await self._rest.fetch_gateway_bot_info()
+        await self._event_manager.dispatch(self._event_factory.deserialize_starting_event())
 
         if shard_count is None:
             shard_count = requirements.shard_count
@@ -1000,10 +1017,6 @@ async def start(
             )
             raise errors.GatewayError("Attempted to start more sessions than were allowed in the given time-window")
 
-        self._is_alive = True
-        # This needs to be started before shards.
-        self._voice.start()
-        self._closing_event = asyncio.Event()
         _LOGGER.info(
             "you can start %s session%s before the next window which starts at %s; planning to start %s session%s... ",
             requirements.session_start_limit.remaining,
@@ -1105,33 +1118,33 @@ def stream(
         -------
         EventStream[hikari.events.base_events.Event]
             The async iterator to handle streamed events. This must be started
-            with `async with stream:` or `await stream.open()` before
+            with `with stream:` or `stream.open()` before
             asynchronously iterating over it.
 
         !!! warning
-            If you use `await stream.open()` to start the stream then you must
-            also close it with `await stream.close()` otherwise it may queue
+            If you use `stream.open()` to start the stream then you must
+            also close it with `stream.close()` otherwise it may queue
             events in memory indefinitely.
 
         Examples
         --------
 
         ```py
-        async with bot.stream(events.ReactionAddEvent, timeout=30).filter(("message_id", message.id)) as stream:
+        with bot.stream(events.ReactionAddEvent, timeout=30).filter(("message_id", message.id)) as stream:
             async for user_id in stream.map("user_id").limit(50):
                 ...
         ```
 
-        or using await `open()` and await `close()`
+        or using `open()` and `close()`
 
         ```py
         stream = bot.stream(events.ReactionAddEvent, timeout=30).filter(("message_id", message.id))
-        await stream.open()
+        stream.open()
 
         async for user_id in stream.map("user_id").limit(50)
             ...
 
-        await stream.close()
+        stream.close()
         ```
 
         See Also
@@ -1139,7 +1152,7 @@ def stream(
         Dispatch: `hikari.impl.bot.GatewayBot.dispatch`
         Listen: `hikari.impl.bot.GatewayBot.listen`
         Subscribe: `hikari.impl.bot.GatewayBot.subscribe`
-        Unubscribe: `hikari.impl.bot.GatewayBot.unsubscribe`
+        Unsubscribe: `hikari.impl.bot.GatewayBot.unsubscribe`
         Wait_for: `hikari.impl.bot.GatewayBot.wait_for`
         """
         self._check_if_alive()
@@ -1178,7 +1191,7 @@ async def on_message(event):
         Dispatch: `hikari.impl.bot.GatewayBot.dispatch`
         Listen: `hikari.impl.bot.GatewayBot.listen`
         Stream: `hikari.impl.bot.GatewayBot.stream`
-        Unubscribe: `hikari.impl.bot.GatewayBot.unsubscribe`
+        Unsubscribe: `hikari.impl.bot.GatewayBot.unsubscribe`
         Wait_for: `hikari.impl.bot.GatewayBot.wait_for`
         """
         self._event_manager.subscribe(event_type, callback)
@@ -1266,7 +1279,7 @@ async def wait_for(
         Listen: `hikari.impl.bot.GatewayBot.listen`
         Stream: `hikari.impl.bot.GatewayBot.stream`
         Subscribe: `hikari.impl.bot.GatewayBot.subscribe`
-        Unubscribe: `hikari.impl.bot.GatewayBot.unsubscribe`
+        Unsubscribe: `hikari.impl.bot.GatewayBot.unsubscribe`
         """
         self._check_if_alive()
         return await self._event_manager.wait_for(event_type, timeout=timeout, predicate=predicate)
diff --git a/hikari/impl/buckets.py b/hikari/impl/buckets.py
index 35e30bd3..a0441ffc 100644
--- a/hikari/impl/buckets.py
+++ b/hikari/impl/buckets.py
@@ -244,19 +244,24 @@ async def __aexit__(
         exc: typing.Optional[BaseException],
         exc_tb: typing.Optional[types.TracebackType],
     ) -> None:
-        self._lock.release()
+        self.release()
 
     @property
     def is_unknown(self) -> bool:
         """Return `builtins.True` if the bucket represents an `UNKNOWN` bucket."""
         return self.name.startswith(UNKNOWN_HASH)
 
+    def release(self) -> None:
+        """Release the lock on the bucket."""
+        self._lock.release()
+
     async def acquire(self) -> None:
-        """Acquire time on this rate limiter.
+        """Acquire time and the lock on this bucket.
 
         !!! note
             You should afterwards invoke `RESTBucket.update_rate_limit` to
-            update any rate limit information you are made aware of.
+            update any rate limit information you are made aware of and
+            `RESTBucket.release` to release the lock.
 
         Raises
         ------
@@ -272,6 +277,8 @@ async def acquire(self) -> None:
         retry_after = self.reset_at - now
 
         if self.is_rate_limited(now) and retry_after > self._max_rate_limit:
+            # Release lock before we error
+            self._lock.release()
             raise errors.RateLimitTooLongError(
                 route=self._compiled_route,
                 retry_after=retry_after,
@@ -304,18 +311,6 @@ def update_rate_limit(self, remaining: int, limit: int, reset_at: float) -> None
         self.reset_at: float = reset_at
         self.period: float = max(0.0, self.reset_at - time.monotonic())
 
-    def drip(self) -> None:
-        """Decrement the remaining count for this bucket.
-
-        !!! note
-            If the bucket is marked as `RESTBucket.is_unknown`, then this will
-            not do anything. `Unknown` buckets have infinite rate limits.
-        """
-        # We don't drip unknown buckets: we cannot rate limit them as we don't know their real bucket hash or
-        # the current rate limit values Discord put on them...
-        if not self.is_unknown:
-            self.remaining -= 1
-
     def resolve(self, real_bucket_hash: str) -> None:
         """Resolve an unknown bucket.
 
@@ -533,7 +528,7 @@ def do_gc_pass(self, expire_after: float) -> None:
 
         _LOGGER.log(ux.TRACE, "purged %s stale buckets, %s remain in survival, %s active", dead, survival, active)
 
-    def acquire(self, compiled_route: routes.CompiledRoute) -> typing.AsyncContextManager[None]:
+    def acquire(self, compiled_route: routes.CompiledRoute) -> RESTBucket:
         """Acquire a bucket for the given route.
 
         Parameters
@@ -543,17 +538,16 @@ def acquire(self, compiled_route: routes.CompiledRoute) -> typing.AsyncContextMa
 
         Returns
         -------
-        typing.AsyncContextManager[builtins.None]
-            A context manager to enter while doing the request.
+        hikari.impl.RESTBucket
+            The bucket for this route.
 
         !!! note
-            You MUST keep the context manager acquired during the whole of the
-            request. From making the request until calling `update_rate_limits`.
+            You MUST keep the context manager of the bucket acquired during the
+            full duration of the request. From making the request until calling
+            `update_rate_limits`.
         """
-        template = compiled_route.route
-
         try:
-            bucket_hash = self.routes_to_hashes[template]
+            bucket_hash = self.routes_to_hashes[compiled_route.route]
             real_bucket_hash = compiled_route.create_real_bucket_hash(bucket_hash)
         except KeyError:
             real_bucket_hash = _create_unknown_hash(compiled_route)
diff --git a/hikari/impl/event_manager_base.py b/hikari/impl/event_manager_base.py
index ed12ebe3..c70d04a7 100644
--- a/hikari/impl/event_manager_base.py
+++ b/hikari/impl/event_manager_base.py
@@ -135,7 +135,7 @@ async def __aenter__(self: _EventStreamT) -> _EventStreamT:
             # This is sync only.
             warnings.warn(
                 "Using EventStream as an async context manager has been deprecated since 2.0.0.dev104. "
-                "Please use it as a sycnrhonous context manager (e.g. with bot.stream(...)) instead.",
+                "Please use it as a synchronous context manager (e.g. with bot.stream(...)) instead.",
                 category=DeprecationWarning,
                 stacklevel=2,
             )
diff --git a/hikari/impl/interaction_server.py b/hikari/impl/interaction_server.py
index e3156a39..77d1f7dd 100644
--- a/hikari/impl/interaction_server.py
+++ b/hikari/impl/interaction_server.py
@@ -235,7 +235,7 @@ async def aiohttp_hook(self, request: aiohttp.web.Request) -> aiohttp.web.Respon
 
         Returns
         -------
-        aiohtttp.web.Response
+        aiohttp.web.Response
             The aiohttp response.
         """
         if request.content_type.lower() != _JSON_CONTENT_TYPE:
diff --git a/hikari/impl/rate_limits.py b/hikari/impl/rate_limits.py
index 54f41847..5818cce1 100644
--- a/hikari/impl/rate_limits.py
+++ b/hikari/impl/rate_limits.py
@@ -250,7 +250,7 @@ class WindowedBurstRateLimiter(BurstRateLimiter):
     Rate limiter for rate limits that last fixed periods of time with a
     fixed number of times it can be used in that time frame.
 
-    To use this, you should call WindowedBurstRateLimiter.aquire` and await the
+    To use this, you should call WindowedBurstRateLimiter.acquire` and await the
     result immediately before performing your rate-limited task.
 
     If the rate limit has been hit, acquiring time will return an incomplete
@@ -307,22 +307,21 @@ async def acquire(self) -> None:
         Calling this function will cause it to block until you are not longer
         being rate limited.
         """
-        loop = asyncio.get_running_loop()
-        future = loop.create_future()
-
         # If we are rate limited, delegate invoking this to the throttler and spin it up
         # if it hasn't started. Likewise, if the throttle task is still running, we should
         # delegate releasing the future to the throttler task so that we still process
         # first-come-first-serve
         if self.throttle_task is not None or self.is_rate_limited(time.monotonic()):
+            loop = asyncio.get_running_loop()
+            future = loop.create_future()
+
             self.queue.append(future)
             if self.throttle_task is None:
                 self.throttle_task = loop.create_task(self.throttle())
+
+            await future
         else:
             self.drip()
-            future.set_result(None)
-
-        await future
 
     def get_time_until_reset(self, now: float) -> float:
         """Determine how long until the current rate limit is reset.
diff --git a/hikari/impl/rest.py b/hikari/impl/rest.py
index e24fe86b..260164a9 100644
--- a/hikari/impl/rest.py
+++ b/hikari/impl/rest.py
@@ -490,7 +490,8 @@ def still_alive(self) -> _LiveAttributes:
         return self
 
 
-@attr.define(auto_exc=True, repr=False, weakref_slot=False)
+# The standard exceptions are all unsloted so slotting here would be a waste of time.
+@attr.define(auto_exc=True, repr=False, slots=False)
 class _RetryRequest(RuntimeError):
     ...
 
@@ -720,6 +721,7 @@ async def _request(
         retry_count = 0
 
         stack = contextlib.AsyncExitStack()
+        trace_logging_enabled = _LOGGER.isEnabledFor(ux.TRACE)
         while True:
             try:
                 uuid = time.uuid()
@@ -732,7 +734,7 @@ async def _request(
                     if not no_auth:
                         await live_attributes.still_alive().global_rate_limit.acquire()
 
-                    if _LOGGER.isEnabledFor(ux.TRACE):
+                    if trace_logging_enabled:
                         _LOGGER.log(
                             ux.TRACE,
                             "%s %s %s\n%s",
@@ -757,7 +759,7 @@ async def _request(
                         proxy_headers=self._proxy_settings.all_headers,
                     )
 
-                    if _LOGGER.isEnabledFor(ux.TRACE):
+                    if trace_logging_enabled:
                         time_taken = (time.monotonic() - start) * 1_000
                         _LOGGER.log(
                             ux.TRACE,
@@ -1330,11 +1332,11 @@ def _build_message_payload(  # noqa: C901- Function too complex
             )
 
         if final_attachments:
-            form = data_binding.URLEncodedFormBuilder(executor=self._executor)
+            form_builder = data_binding.URLEncodedFormBuilder(executor=self._executor)
 
             for i, attachment in enumerate(final_attachments):
-                form.add_resource(f"file{i}", attachment)
-            return body, form
+                form_builder.add_resource(f"file{i}", attachment)
+            return body, form_builder
 
         return body, None
 
@@ -1362,7 +1364,7 @@ async def create_message(
         ] = undefined.UNDEFINED,
     ) -> messages_.Message:
         route = routes.POST_CHANNEL_MESSAGES.compile(channel=channel)
-        body, form = self._build_message_payload(
+        body, form_builder = self._build_message_payload(
             content=content,
             attachment=attachment,
             attachments=attachments,
@@ -1379,9 +1381,9 @@ async def create_message(
         body.put("nonce", nonce)
         body.put("message_reference", reply, conversion=lambda m: {"message_id": str(int(m))})
 
-        if form is not None:
-            form.add_field("payload_json", data_binding.dump_json(body), content_type=_APPLICATION_JSON)
-            response = await self._request(route, form_builder=form)
+        if form_builder is not None:
+            form_builder.add_field("payload_json", data_binding.dump_json(body), content_type=_APPLICATION_JSON)
+            response = await self._request(route, form_builder=form_builder)
         else:
             response = await self._request(route, json=body)
 
@@ -1426,7 +1428,7 @@ async def edit_message(
         flags: typing.Union[undefined.UndefinedType, int, messages_.MessageFlag] = undefined.UNDEFINED,
     ) -> messages_.Message:
         route = routes.PATCH_CHANNEL_MESSAGE.compile(channel=channel, message=message)
-        body, form = self._build_message_payload(
+        body, form_builder = self._build_message_payload(
             content=content,
             attachment=attachment,
             attachments=attachments,
@@ -1443,9 +1445,9 @@ async def edit_message(
             edit=True,
         )
 
-        if form is not None:
-            form.add_field("payload_json", data_binding.dump_json(body), content_type=_APPLICATION_JSON)
-            response = await self._request(route, form_builder=form)
+        if form_builder is not None:
+            form_builder.add_field("payload_json", data_binding.dump_json(body), content_type=_APPLICATION_JSON)
+            response = await self._request(route, form_builder=form_builder)
         else:
             response = await self._request(route, json=body)
 
@@ -1755,7 +1757,7 @@ async def execute_webhook(
         query = data_binding.StringMapBuilder()
         query.put("wait", True)
 
-        body, form = self._build_message_payload(
+        body, form_builder = self._build_message_payload(
             content=content,
             attachment=attachment,
             attachments=attachments,
@@ -1772,9 +1774,9 @@ async def execute_webhook(
         body.put("username", username)
         body.put("avatar_url", avatar_url, conversion=str)
 
-        if form is not None:
-            form.add_field("payload_json", data_binding.dump_json(body), content_type=_APPLICATION_JSON)
-            response = await self._request(route, form_builder=form, query=query, no_auth=True)
+        if form_builder is not None:
+            form_builder.add_field("payload_json", data_binding.dump_json(body), content_type=_APPLICATION_JSON)
+            response = await self._request(route, form_builder=form_builder, query=query, no_auth=True)
         else:
             response = await self._request(route, json=body, query=query, no_auth=True)
 
@@ -1822,7 +1824,7 @@ async def edit_webhook_message(
         webhook_id = webhook if isinstance(webhook, int) else webhook.webhook_id
         route = routes.PATCH_WEBHOOK_MESSAGE.compile(webhook=webhook_id, token=token, message=message)
 
-        body, form = self._build_message_payload(
+        body, form_builder = self._build_message_payload(
             content=content,
             attachment=attachment,
             attachments=attachments,
@@ -1837,9 +1839,9 @@ async def edit_webhook_message(
             edit=True,
         )
 
-        if form is not None:
-            form.add_field("payload_json", data_binding.dump_json(body), content_type=_APPLICATION_JSON)
-            response = await self._request(route, form_builder=form, no_auth=True)
+        if form_builder is not None:
+            form_builder.add_field("payload_json", data_binding.dump_json(body), content_type=_APPLICATION_JSON)
+            response = await self._request(route, form_builder=form_builder, no_auth=True)
         else:
             response = await self._request(route, json=body, no_auth=True)
 
@@ -1981,11 +1983,13 @@ async def authorize_client_credentials_token(
         scopes: typing.Sequence[typing.Union[applications.OAuth2Scope, str]],
     ) -> applications.PartialOAuth2Token:
         route = routes.POST_TOKEN.compile()
-        form = data_binding.URLEncodedFormBuilder()
-        form.add_field("grant_type", "client_credentials")
-        form.add_field("scope", " ".join(scopes))
+        form_builder = data_binding.URLEncodedFormBuilder()
+        form_builder.add_field("grant_type", "client_credentials")
+        form_builder.add_field("scope", " ".join(scopes))
 
-        response = await self._request(route, form_builder=form, auth=self._gen_oauth2_token(client, client_secret))
+        response = await self._request(
+            route, form_builder=form_builder, auth=self._gen_oauth2_token(client, client_secret)
+        )
         assert isinstance(response, dict)
         return self._entity_factory.deserialize_partial_token(response)
 
@@ -1997,12 +2001,14 @@ async def authorize_access_token(
         redirect_uri: str,
     ) -> applications.OAuth2AuthorizationToken:
         route = routes.POST_TOKEN.compile()
-        form = data_binding.URLEncodedFormBuilder()
-        form.add_field("grant_type", "authorization_code")
-        form.add_field("code", code)
-        form.add_field("redirect_uri", redirect_uri)
+        form_builder = data_binding.URLEncodedFormBuilder()
+        form_builder.add_field("grant_type", "authorization_code")
+        form_builder.add_field("code", code)
+        form_builder.add_field("redirect_uri", redirect_uri)
 
-        response = await self._request(route, form_builder=form, auth=self._gen_oauth2_token(client, client_secret))
+        response = await self._request(
+            route, form_builder=form_builder, auth=self._gen_oauth2_token(client, client_secret)
+        )
         assert isinstance(response, dict)
         return self._entity_factory.deserialize_authorization_token(response)
 
@@ -2017,14 +2023,16 @@ async def refresh_access_token(
         ] = undefined.UNDEFINED,
     ) -> applications.OAuth2AuthorizationToken:
         route = routes.POST_TOKEN.compile()
-        form = data_binding.URLEncodedFormBuilder()
-        form.add_field("grant_type", "refresh_token")
-        form.add_field("refresh_token", refresh_token)
+        form_builder = data_binding.URLEncodedFormBuilder()
+        form_builder.add_field("grant_type", "refresh_token")
+        form_builder.add_field("refresh_token", refresh_token)
 
         if scopes is not undefined.UNDEFINED:
-            form.add_field("scope", " ".join(scopes))
+            form_builder.add_field("scope", " ".join(scopes))
 
-        response = await self._request(route, form_builder=form, auth=self._gen_oauth2_token(client, client_secret))
+        response = await self._request(
+            route, form_builder=form_builder, auth=self._gen_oauth2_token(client, client_secret)
+        )
         assert isinstance(response, dict)
         return self._entity_factory.deserialize_authorization_token(response)
 
@@ -2035,9 +2043,9 @@ async def revoke_access_token(
         token: typing.Union[str, applications.PartialOAuth2Token],
     ) -> None:
         route = routes.POST_TOKEN_REVOKE.compile()
-        form = data_binding.URLEncodedFormBuilder()
-        form.add_field("token", str(token))
-        await self._request(route, form_builder=form, auth=self._gen_oauth2_token(client, client_secret))
+        form_builder = data_binding.URLEncodedFormBuilder()
+        form_builder.add_field("token", str(token))
+        await self._request(route, form_builder=form_builder, auth=self._gen_oauth2_token(client, client_secret))
 
     async def add_user_to_guild(
         self,
@@ -3473,7 +3481,7 @@ async def edit_interaction_response(
     ) -> messages_.Message:
         route = routes.PATCH_INTERACTION_RESPONSE.compile(webhook=application, token=token)
 
-        body, form = self._build_message_payload(
+        body, form_builder = self._build_message_payload(
             content=content,
             attachment=attachment,
             attachments=attachments,
@@ -3488,9 +3496,9 @@ async def edit_interaction_response(
             edit=True,
         )
 
-        if form is not None:
-            form.add_field("payload_json", data_binding.dump_json(body), content_type=_APPLICATION_JSON)
-            response = await self._request(route, form_builder=form, no_auth=True)
+        if form_builder is not None:
+            form_builder.add_field("payload_json", data_binding.dump_json(body), content_type=_APPLICATION_JSON)
+            response = await self._request(route, form_builder=form_builder, no_auth=True)
         else:
             response = await self._request(route, json=body, no_auth=True)
 
diff --git a/hikari/impl/rest_bot.py b/hikari/impl/rest_bot.py
index a531b195..4f83bff3 100644
--- a/hikari/impl/rest_bot.py
+++ b/hikari/impl/rest_bot.py
@@ -316,7 +316,12 @@ def executor(self) -> typing.Optional[concurrent.futures.Executor]:
         return self._executor
 
     @staticmethod
-    def print_banner(banner: typing.Optional[str], allow_color: bool, force_color: bool) -> None:
+    def print_banner(
+        banner: typing.Optional[str],
+        allow_color: bool,
+        force_color: bool,
+        extra_args: typing.Optional[typing.Dict[str, str]] = None,
+    ) -> None:
         """Print the banner.
 
         This allows library vendors to override this behaviour, or choose to
@@ -341,8 +346,16 @@ def print_banner(banner: typing.Optional[str], allow_color: bool, force_color: b
 
         !!! note
             `force_color` will always take precedence over `allow_color`.
+        extra_args : typing.Optional[typing.Dict[builtins.str, builtins.str]]
+            If provided, extra $-substitutions to use when printing the banner.
+            Default substitutions can not be overwritten.
+
+        Raises
+        ------
+        builtins.ValueError
+            If `extra_args` contains a default $-substitution.
         """
-        ux.print_banner(banner, allow_color, force_color)
+        ux.print_banner(banner, allow_color, force_color, extra_args=extra_args)
 
     async def close(self) -> None:
         if not self._close_event:
@@ -402,7 +415,7 @@ def run(
         close_loop : builtins.bool
             Defaults to `builtins.True`. If `builtins.True`, then once the bot
             enters a state where all components have shut down permanently
-            during application shutdown, then all asyngens and background tasks
+            during application shutdown, then all asyncgens and background tasks
             will be destroyed, and the event loop will be shut down.
 
             This will wait until all `hikari`-owned `aiohttp` connectors have
diff --git a/hikari/impl/shard.py b/hikari/impl/shard.py
index 036ca75e..05e8ee56 100644
--- a/hikari/impl/shard.py
+++ b/hikari/impl/shard.py
@@ -112,6 +112,7 @@ def filterer(entry: str) -> str:
     _ZlibDecompressor = zlib._Decompress
 
 
+# aiohttp.ClientWebSocketResponse isn't slotted
 @typing.final
 class _GatewayTransport(aiohttp.ClientWebSocketResponse):
     """Internal component to handle lower-level communication logic.
@@ -123,8 +124,6 @@ class _GatewayTransport(aiohttp.ClientWebSocketResponse):
     Payload logging is also performed here.
     """
 
-    __slots__: typing.Sequence[str] = ("zlib", "logger", "log_filterer", "sent_close")
-
     def __init__(self, *args: typing.Any, **kwargs: typing.Any) -> None:
         super().__init__(*args, **kwargs)
         self.zlib: _ZlibDecompressor = zlib.decompressobj()
@@ -509,6 +508,7 @@ def shard_count(self) -> int:
         return self._shard_count
 
     async def close(self) -> None:
+        self._check_if_alive()
         if not self._closing_event.is_set():
             try:
                 if self._ws is not None:
@@ -538,7 +538,7 @@ def _get_ws(self) -> _GatewayTransport:
         return self._ws
 
     async def join(self) -> None:
-        """Wait for this shard to close, if running."""
+        self._check_if_alive()
         await self._closed_event.wait()
 
     async def _send_json(
@@ -823,7 +823,7 @@ async def _run(self) -> None:
                         return
 
                 except errors.GatewayConnectionError as ex:
-                    self._logger.error(
+                    self._logger.warning(
                         "failed to communicate with server, reason was: %r. Will retry shortly", ex.reason
                     )
 
diff --git a/hikari/impl/special_endpoints.py b/hikari/impl/special_endpoints.py
index bb63e863..9d6fdc4f 100644
--- a/hikari/impl/special_endpoints.py
+++ b/hikari/impl/special_endpoints.py
@@ -80,7 +80,6 @@
     _T = typing.TypeVar("_T")
     _CommandBuilderT = typing.TypeVar("_CommandBuilderT", bound="CommandBuilder")
     _SlashCommandBuilderT = typing.TypeVar("_SlashCommandBuilderT", bound="SlashCommandBuilder")
-    _ContextMenuCommandBuilderT = typing.TypeVar("_ContextMenuCommandBuilderT", bound="ContextMenuCommandBuilder")
     _InteractionMessageBuilderT = typing.TypeVar("_InteractionMessageBuilderT", bound="InteractionMessageBuilder")
     _InteractionDeferredBuilderT = typing.TypeVar("_InteractionDeferredBuilderT", bound="InteractionDeferredBuilder")
     _InteractionAutocompleteBuilderT = typing.TypeVar(
diff --git a/hikari/internal/data_binding.py b/hikari/internal/data_binding.py
index 249d6270..e6550e13 100644
--- a/hikari/internal/data_binding.py
+++ b/hikari/internal/data_binding.py
@@ -49,7 +49,7 @@
     import concurrent
     import contextlib
 
-    T = typing.TypeVar("T", covariant=True)
+    T_co = typing.TypeVar("T_co", covariant=True)
 
 Headers = typing.Mapping[str, str]
 """Type hint for HTTP headers."""
@@ -170,10 +170,10 @@ def put(
     def put(
         self,
         key: str,
-        value: undefined.UndefinedOr[T],
+        value: undefined.UndefinedOr[T_co],
         /,
         *,
-        conversion: typing.Callable[[T], Stringish],
+        conversion: typing.Callable[[T_co], Stringish],
     ) -> None:
         ...
 
@@ -255,10 +255,10 @@ def put(self, key: str, value: undefined.UndefinedNoneOr[JSONish], /) -> None:
     def put(
         self,
         key: str,
-        value: undefined.UndefinedNoneOr[T],
+        value: undefined.UndefinedNoneOr[T_co],
         /,
         *,
-        conversion: typing.Callable[[T], JSONish],
+        conversion: typing.Callable[[T_co], JSONish],
     ) -> None:
         ...
 
@@ -309,10 +309,10 @@ def put_array(
     def put_array(
         self,
         key: str,
-        values: undefined.UndefinedOr[typing.Iterable[T]],
+        values: undefined.UndefinedOr[typing.Iterable[T_co]],
         /,
         *,
-        conversion: typing.Callable[[T], JSONish],
+        conversion: typing.Callable[[T_co], JSONish],
     ) -> None:
         ...
 
@@ -334,7 +334,7 @@ def put_array(
         ----------
         key : builtins.str
             The key to give the element.
-        values : hikari.undefined.UndefinedOr[typing.Iterable[T]]
+        values : hikari.undefined.UndefinedOr[typing.Iterable[T_co]]
             The JSON types to put. This may be an iterable of non-JSON types if
             a conversion is also specified. This may alternatively be undefined.
             In the latter case, nothing is performed.
diff --git a/hikari/internal/enums.py b/hikari/internal/enums.py
index 3fefaae9..25746880 100644
--- a/hikari/internal/enums.py
+++ b/hikari/internal/enums.py
@@ -696,7 +696,7 @@ def split(self: _T) -> typing.Sequence[_T]:
 
         Any unrecognised bits will be omitted for brevity.
 
-        The result will be a name-sorted `typing.Sequence` of each membe
+        The result will be a name-sorted `typing.Sequence` of each member
         """
         return sorted(
             (member for member in self.__class__._powers_of_2_to_member_map_.values() if member.value & self),
diff --git a/hikari/internal/time.py b/hikari/internal/time.py
index 26740f9b..813e479b 100644
--- a/hikari/internal/time.py
+++ b/hikari/internal/time.py
@@ -201,12 +201,12 @@ def utc_datetime() -> datetime.datetime:
 
 # time.monotonic_ns is no slower than time.monotonic, but is more accurate.
 # Also, fun fact that monotonic_ns appears to be 1µs faster on average than
-# monotonic on AARM64 architectures, but on x86, monotonic is around 1ns faster
+# monotonic on ARM64 architectures, but on x86, monotonic is around 1ns faster
 # than monotonic_ns. Just thought that was kind of interesting to note down.
 # (RPi 3B versus i7 6700)
 
-# time.perf_counter and time.perf_counter_ns dont have proper typehints, causing
-# pdoc to not be able to recognice them. This is just a little hack around that.
+# time.perf_counter and time.perf_counter_ns don't have proper typehints, causing
+# pdoc to not be able to recognise them. This is just a little hack around that.
 def monotonic() -> float:
     """Performance counter for benchmarking."""  # noqa: D401 - Imperative mood
     return time.perf_counter()
diff --git a/hikari/internal/ux.py b/hikari/internal/ux.py
index e3a30690..b3a9f695 100644
--- a/hikari/internal/ux.py
+++ b/hikari/internal/ux.py
@@ -148,7 +148,12 @@ def init_logging(
 """Set of env variables which always indicate that ANSI flags should be included."""
 
 
-def print_banner(package: typing.Optional[str], allow_color: bool, force_color: bool) -> None:
+def print_banner(
+    package: typing.Optional[str],
+    allow_color: bool,
+    force_color: bool,
+    extra_args: typing.Optional[typing.Dict[str, str]] = None,
+) -> None:
     """Print a banner of choice to `sys.stdout`.
 
     Inspired by Spring Boot, we display an ASCII logo on startup. This is styled
@@ -172,6 +177,14 @@ def print_banner(package: typing.Optional[str], allow_color: bool, force_color:
         If `builtins.True`, return `builtins.True` always, otherwise only
         return `builtins.True` if the device supports colour output and the
         `allow_color` flag is not `builtins.False`.
+    extra_args : typing.Optional[typing.Dict[builtins.str, builtins.str]]
+        If provided, extra $-substitutions to use when printing the banner.
+        Default substitutions can not be overwritten.
+
+    Raises
+    ------
+    builtins.ValueError
+        If `extra_args` contains a default $-substitution.
     """
     if package is None:
         return
@@ -198,6 +211,12 @@ def print_banner(package: typing.Optional[str], allow_color: bool, force_color:
         "system_description": " ".join(filtered_system_bits),
     }
 
+    if extra_args:
+        for key in extra_args:
+            if key in args:
+                raise ValueError(f"Cannot overwrite $-substitution `{key}`. Please use a different key.")
+        args.update(extra_args)
+
     if supports_color(allow_color, force_color):
         args.update(colorlog.escape_codes.escape_codes)
     else:
diff --git a/hikari/presences.py b/hikari/presences.py
index 8c03e74c..7a6248f4 100644
--- a/hikari/presences.py
+++ b/hikari/presences.py
@@ -170,7 +170,7 @@ def large_image_url(self) -> typing.Optional[files.URL]:
 
         !!! note
             This will be `builtins.None` if no large image asset exists or if the
-            asset's dymamic URL (indicated by a `{name}:` prefix) is not known.
+            asset's dynamic URL (indicated by a `{name}:` prefix) is not known.
         """
         try:
             return self.make_large_image_url()
@@ -215,7 +215,7 @@ def small_image_url(self) -> typing.Optional[files.URL]:
 
         !!! note
             This will be `builtins.None` if no large image asset exists or if the
-            asset's dymamic URL (indicated by a `{name}:` prefix) is not known.
+            asset's dynamic URL (indicated by a `{name}:` prefix) is not known.
         """
         try:
             return self.make_small_image_url()
@@ -295,7 +295,7 @@ class ActivityFlag(enums.Flag):
     """Party privacy: friends only."""
 
     PARTY_PRIVACY_VOICE_CHANNEL = 1 << 7
-    """Party prvacy: voice channel only."""
+    """Party privacy: voice channel only."""
 
     EMBEDDED = 1 << 8
     """An activity that's embedded into a voice channel."""
diff --git a/hikari/snowflakes.py b/hikari/snowflakes.py
index 501d63a0..6e97be91 100644
--- a/hikari/snowflakes.py
+++ b/hikari/snowflakes.py
@@ -218,7 +218,7 @@ def calculate_shard_id(
 
 The valid types for this type hint are:
 
-- `buitlins.int`
+- `builtins.int`
 - `Snowflake`
 """
 
@@ -235,7 +235,7 @@ def calculate_shard_id(
 
 The valid types for this type hint are:
 
-- `buitlins.int`
+- `builtins.int`
 - `Snowflake`
 - `datetime.datetime`
 """
diff --git a/pipelines/config.py b/pipelines/config.py
index 81b047dd..0e917e74 100644
--- a/pipelines/config.py
+++ b/pipelines/config.py
@@ -37,7 +37,6 @@
 FLAKE8_REPORT = "public/flake8"
 PYPROJECT_TOML = "pyproject.toml"
 COVERAGE_HTML_PATH = _os.path.join(ARTIFACT_DIRECTORY, "coverage", "html")
-PYRIGHT_VERSION = "1.1.190"  # 2021-11-25 (UTC+0)
 
 # Reformatting paths
 
diff --git a/pipelines/nox.py b/pipelines/nox.py
index 9cbef36a..f55ddc8e 100644
--- a/pipelines/nox.py
+++ b/pipelines/nox.py
@@ -32,7 +32,7 @@
 from pipelines import config
 
 # Default sessions should be defined here
-_options.sessions = ["reformat-code", "pytest", "flake8", "mypy", "verify-types"]
+_options.sessions = ["reformat-code", "pytest", "flake8", "slotscheck", "mypy", "verify-types"]
 
 _NoxCallbackSig = typing.Callable[[Session], None]
 
diff --git a/pipelines/pyright.nox.py b/pipelines/pyright.nox.py
index e91abed1..6b61b2cc 100644
--- a/pipelines/pyright.nox.py
+++ b/pipelines/pyright.nox.py
@@ -31,6 +31,5 @@ def verify_types(session: nox.Session) -> None:
     session.install("-r", "dev-requirements.txt")
     session.install(".")
     # session.env["PYRIGHT_PYTHON_GLOBAL_NODE"] = "off"
-    session.env["PYRIGHT_PYTHON_FORCE_VERSION"] = config.PYRIGHT_VERSION
     session.run("python", "-m", "pyright", "--version")
-    session.run("python", "-m", "pyright", "--verifytypes", "hikari", "--ignoreexternal")
+    session.run("python", "-m", "pyright", "--verifytypes", config.MAIN_PACKAGE, "--ignoreexternal")
diff --git a/pipelines/slotscheck.nox.py b/pipelines/slotscheck.nox.py
new file mode 100644
index 00000000..67531fcd
--- /dev/null
+++ b/pipelines/slotscheck.nox.py
@@ -0,0 +1,32 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020 Nekokatt
+# Copyright (c) 2021-present davfsa
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+"""Check for common slotting mistakes."""
+
+from pipelines import config
+from pipelines import nox
+
+
+@nox.session()
+def slotscheck(session: nox.Session) -> None:
+    """Check for common slotting mistakes."""
+    session.install(".", "-r", "dev-requirements.txt")
+    session.run("slotscheck", "-m", config.MAIN_PACKAGE)
diff --git a/pyproject.toml b/pyproject.toml
index c421d77e..19a3e06d 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -132,3 +132,14 @@ warn_unused_ignores = true
 [[tool.mypy.overrides]]
 module = 'rillrate.*'
 ignore_missing_imports = true
+
+[tool.slotscheck]
+strict-imports = true
+require-superclass = true
+require-subclass = true
+exclude-classes = """
+(
+    ^hikari\\.internal\\.enums:(Enum|Flag|_EnumMeta|_FlagMeta)$
+    |^hikari\\.files:MultiprocessingFileReader$
+)
+"""


Test patch:
diff --git a/tests/hikari/impl/test_bot.py b/tests/hikari/impl/test_bot.py
index 8d1cc85e..1663a9cf 100644
--- a/tests/hikari/impl/test_bot.py
+++ b/tests/hikari/impl/test_bot.py
@@ -372,7 +372,8 @@ async def test__close_when_already_closing(self, bot):
         assert bot._is_alive is True
 
     @pytest.mark.asyncio()
-    async def test__close(self, bot, event_manager, event_factory, rest, voice, cache):
+    @pytest.mark.parametrize("is_alive", [True, False])
+    async def test__close(self, bot, event_manager, event_factory, rest, voice, cache, is_alive):
         def null_call(arg):
             return arg
 
@@ -409,7 +410,7 @@ def assert_awaited_once(self):
         voice.close = AwaitableMock()
         bot._closing_event = closing_event = mock.Mock(is_set=mock.Mock(return_value=False))
         bot._closed_event = None
-        bot._is_alive = True
+        bot._is_alive = is_alive
         error = RuntimeError()
         shard0 = mock.Mock(id=0, close=AwaitableMock())
         shard1 = mock.Mock(id=1, close=AwaitableMock(error))
@@ -435,7 +436,6 @@ def assert_awaited_once(self):
                 mock.call(shard1.close()),
                 mock.call(shard2.close()),
             ],
-            any_order=False,
         )
 
         rest.close.assert_awaited_once()
@@ -458,14 +458,16 @@ def assert_awaited_once(self):
         assert bot._shards == {}
         cache.clear.assert_called_once_with()
 
-        # Dispatching events in the right order
-        event_manager.dispatch.assert_has_calls(
-            [
-                mock.call(event_factory.deserialize_stopping_event.return_value),
-                mock.call(event_factory.deserialize_stopped_event.return_value),
-            ],
-            any_order=False,
-        )
+        if is_alive:
+            # Dispatching events in the right order
+            event_manager.dispatch.assert_has_calls(
+                [
+                    mock.call(event_factory.deserialize_stopping_event.return_value),
+                    mock.call(event_factory.deserialize_stopped_event.return_value),
+                ],
+            )
+        else:
+            event_manager.dispatch.assert_not_called()
 
     def test_dispatch(self, bot, event_manager):
         event = object()
@@ -528,9 +530,9 @@ def test_listen(self, bot, event_manager):
 
     def test_print_banner(self, bot):
         with mock.patch.object(ux, "print_banner") as print_banner:
-            bot.print_banner("testing", False, True)
+            bot.print_banner("testing", False, True, extra_args={"test_key": "test_value"})
 
-        print_banner.assert_called_once_with("testing", False, True)
+        print_banner.assert_called_once_with("testing", False, True, extra_args={"test_key": "test_value"})
 
     def test_run_when_already_running(self, bot):
         bot._is_alive = True
diff --git a/tests/hikari/impl/test_buckets.py b/tests/hikari/impl/test_buckets.py
index a6a65965..4984250e 100644
--- a/tests/hikari/impl/test_buckets.py
+++ b/tests/hikari/impl/test_buckets.py
@@ -20,6 +20,7 @@
 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 # SOFTWARE.
 import asyncio
+import contextlib
 import time
 
 import mock
@@ -44,20 +45,27 @@ def compiled_route(self, template):
 
     @pytest.mark.asyncio()
     async def test_async_context_manager(self, compiled_route):
-        with mock.patch.object(asyncio, "Lock") as lock:
-            lock.return_value.acquire = mock.AsyncMock()
-            with mock.patch.object(buckets.RESTBucket, "acquire", new=mock.AsyncMock()) as acquire:
+        with mock.patch.object(buckets.RESTBucket, "acquire", new=mock.AsyncMock()) as acquire:
+            with mock.patch.object(buckets.RESTBucket, "release") as release:
                 async with buckets.RESTBucket("spaghetti", compiled_route, float("inf")):
                     acquire.assert_awaited_once_with()
-                    lock.return_value.release.assert_not_called()
+                    release.assert_not_called()
 
-                lock.return_value.release.assert_called_once_with()
+            release.assert_called_once_with()
 
     @pytest.mark.parametrize("name", ["spaghetti", buckets.UNKNOWN_HASH])
     def test_is_unknown(self, name, compiled_route):
         with buckets.RESTBucket(name, compiled_route, float("inf")) as rl:
             assert rl.is_unknown is (name == buckets.UNKNOWN_HASH)
 
+    def test_release(self, compiled_route):
+        with buckets.RESTBucket(__name__, compiled_route, float("inf")) as rl:
+            rl._lock = mock.Mock()
+
+            rl.release()
+
+            rl._lock.release.assert_called_once_with()
+
     def test_update_rate_limit(self, compiled_route):
         with buckets.RESTBucket(__name__, compiled_route, float("inf")) as rl:
             rl.remaining = 1
@@ -73,13 +81,6 @@ def test_update_rate_limit(self, compiled_route):
             assert rl.reset_at == 27
             assert rl.period == 27 - 4.20
 
-    @pytest.mark.parametrize("name", ["spaghetti", buckets.UNKNOWN_HASH])
-    def test_drip(self, name, compiled_route):
-        with buckets.RESTBucket(name, compiled_route, float("inf")) as rl:
-            rl.remaining = 1
-            rl.drip()
-            assert rl.remaining == 0 if name != buckets.UNKNOWN_HASH else 1
-
     @pytest.mark.asyncio()
     async def test_acquire_when_unknown_bucket(self, compiled_route):
         with buckets.RESTBucket(buckets.UNKNOWN_HASH, compiled_route, float("inf")) as rl:
@@ -92,11 +93,18 @@ async def test_acquire_when_unknown_bucket(self, compiled_route):
 
     @pytest.mark.asyncio()
     async def test_acquire_when_too_long_ratelimit(self, compiled_route):
-        with buckets.RESTBucket("spaghetti", compiled_route, 60) as rl:
-            rl.reset_at = time.perf_counter() + 999999999999999999999999999
-            with mock.patch.object(buckets.RESTBucket, "is_rate_limited", return_value=True):
-                with pytest.raises(errors.RateLimitTooLongError):
-                    await rl.acquire()
+        stack = contextlib.ExitStack()
+        rl = stack.enter_context(buckets.RESTBucket("spaghetti", compiled_route, 60))
+        rl._lock = mock.Mock(acquire=mock.AsyncMock())
+        rl.reset_at = time.perf_counter() + 999999999999999999999999999
+        stack.enter_context(mock.patch.object(buckets.RESTBucket, "is_rate_limited", return_value=True))
+        stack.enter_context(pytest.raises(errors.RateLimitTooLongError))
+
+        with stack:
+            await rl.acquire()
+
+        rl._lock.acquire.assert_awaited_once_with()
+        rl._lock.release.assert_called_once_with()
 
     @pytest.mark.asyncio()
     async def test_acquire(self, compiled_route):
@@ -272,7 +280,7 @@ async def test_acquire_route_when_not_in_routes_to_real_hashes_makes_new_bucket_
     async def test_acquire_route_when_not_in_routes_to_real_hashes_doesnt_cache_route(self):
         with buckets.RESTBucketManager(max_rate_limit=float("inf")) as mgr:
             route = mock.Mock()
-            route.create_real_bucket_hash = mock.Mock(wraps=lambda intial_hash: intial_hash + ";bobs")
+            route.create_real_bucket_hash = mock.Mock(wraps=lambda initial_hash: initial_hash + ";bobs")
 
             mgr.acquire(route)
 
@@ -296,7 +304,7 @@ async def test_acquire_route_returns_context_manager(self):
 
             bucket = mock.Mock(reset_at=time.perf_counter() + 999999999999999999999999999)
             with mock.patch.object(buckets, "RESTBucket", return_value=bucket):
-                route.create_real_bucket_hash = mock.Mock(wraps=lambda intial_hash: intial_hash + ";bobs")
+                route.create_real_bucket_hash = mock.Mock(wraps=lambda initial_hash: initial_hash + ";bobs")
 
                 assert mgr.acquire(route) is bucket
 
@@ -315,7 +323,7 @@ async def test_acquire_unknown_route_returns_context_manager_for_new_bucket(self
     async def test_update_rate_limits_if_wrong_bucket_hash_reroutes_route(self):
         with buckets.RESTBucketManager(max_rate_limit=float("inf")) as mgr:
             route = mock.Mock()
-            route.create_real_bucket_hash = mock.Mock(wraps=lambda intial_hash: intial_hash + ";bobs")
+            route.create_real_bucket_hash = mock.Mock(wraps=lambda initial_hash: initial_hash + ";bobs")
             mgr.routes_to_hashes[route.route] = "123"
 
             with mock.patch.object(hikari_date, "monotonic", return_value=27):
@@ -330,7 +338,7 @@ async def test_update_rate_limits_if_wrong_bucket_hash_reroutes_route(self):
     async def test_update_rate_limits_if_unknown_bucket_hash_reroutes_route(self):
         with buckets.RESTBucketManager(max_rate_limit=float("inf")) as mgr:
             route = mock.Mock()
-            route.create_real_bucket_hash = mock.Mock(wraps=lambda intial_hash: intial_hash + ";bobs")
+            route.create_real_bucket_hash = mock.Mock(wraps=lambda initial_hash: initial_hash + ";bobs")
             mgr.routes_to_hashes[route.route] = "123"
             bucket = mock.Mock()
             mgr.real_hashes_to_buckets["UNKNOWN;bobs"] = bucket
@@ -349,7 +357,7 @@ async def test_update_rate_limits_if_unknown_bucket_hash_reroutes_route(self):
     async def test_update_rate_limits_if_right_bucket_hash_does_nothing_to_hash(self):
         with buckets.RESTBucketManager(max_rate_limit=float("inf")) as mgr:
             route = mock.Mock()
-            route.create_real_bucket_hash = mock.Mock(wraps=lambda intial_hash: intial_hash + ";bobs")
+            route.create_real_bucket_hash = mock.Mock(wraps=lambda initial_hash: initial_hash + ";bobs")
             mgr.routes_to_hashes[route.route] = "123"
             bucket = mock.Mock(reset_at=time.perf_counter() + 999999999999999999999999999)
             mgr.real_hashes_to_buckets["123;bobs"] = bucket
@@ -365,7 +373,7 @@ async def test_update_rate_limits_if_right_bucket_hash_does_nothing_to_hash(self
     async def test_update_rate_limits_updates_params(self):
         with buckets.RESTBucketManager(max_rate_limit=float("inf")) as mgr:
             route = mock.Mock()
-            route.create_real_bucket_hash = mock.Mock(wraps=lambda intial_hash: intial_hash + ";bobs")
+            route.create_real_bucket_hash = mock.Mock(wraps=lambda initial_hash: initial_hash + ";bobs")
             mgr.routes_to_hashes[route.route] = "123"
             bucket = mock.Mock(reset_at=time.perf_counter() + 999999999999999999999999999)
             mgr.real_hashes_to_buckets["123;bobs"] = bucket
diff --git a/tests/hikari/impl/test_rate_limits.py b/tests/hikari/impl/test_rate_limits.py
index 059997a4..20783737 100644
--- a/tests/hikari/impl/test_rate_limits.py
+++ b/tests/hikari/impl/test_rate_limits.py
@@ -210,13 +210,12 @@ async def test_drip_if_not_throttled_and_not_ratelimited(self, ratelimiter, even
         ratelimiter.drip = mock.Mock()
         ratelimiter.throttle_task = None
         ratelimiter.is_rate_limited = mock.Mock(return_value=False)
-        future = MockFuture()
-        event_loop.create_future = mock.Mock(return_value=future)
+        event_loop.create_future = mock.Mock()
 
         await ratelimiter.acquire()
 
         ratelimiter.drip.assert_called_once_with()
-        future.set_result.assert_called_once_with(None)
+        event_loop.create_future.assert_not_called()
 
     @pytest.mark.asyncio()
     async def test_no_drip_if_throttle_task_is_not_None(self, ratelimiter, event_loop):
diff --git a/tests/hikari/impl/test_rest.py b/tests/hikari/impl/test_rest.py
index a07c1d35..ed9f269a 100644
--- a/tests/hikari/impl/test_rest.py
+++ b/tests/hikari/impl/test_rest.py
@@ -3334,8 +3334,7 @@ async def test_fetch_voice_regions(self, rest_client):
         rest_client._request.assert_awaited_once_with(expected_route)
         assert rest_client._entity_factory.deserialize_voice_region.call_count == 2
         rest_client._entity_factory.deserialize_voice_region.assert_has_calls(
-            [mock.call({"id": "123"}), mock.call({"id": "456"})],
-            any_order=False,
+            [mock.call({"id": "123"}), mock.call({"id": "456"})]
         )
 
     async def test_fetch_user(self, rest_client):
diff --git a/tests/hikari/impl/test_rest_bot.py b/tests/hikari/impl/test_rest_bot.py
index b825389f..da8e0fb5 100644
--- a/tests/hikari/impl/test_rest_bot.py
+++ b/tests/hikari/impl/test_rest_bot.py
@@ -232,9 +232,9 @@ def test_is_alive_property(self, mock_rest_bot, close_event, expected):
 
     def test_print_banner(self, mock_rest_bot):
         with mock.patch.object(ux, "print_banner") as print_banner:
-            mock_rest_bot.print_banner("okokok", True, False)
+            mock_rest_bot.print_banner("okokok", True, False, {"test_key": "test_value"})
 
-            print_banner.assert_called_once_with("okokok", True, False)
+            print_banner.assert_called_once_with("okokok", True, False, extra_args={"test_key": "test_value"})
 
     @pytest.mark.asyncio()
     async def test_close(self, mock_rest_bot, mock_interaction_server, mock_rest_client):
diff --git a/tests/hikari/impl/test_shard.py b/tests/hikari/impl/test_shard.py
index ab5fa4b9..6f29451c 100644
--- a/tests/hikari/impl/test_shard.py
+++ b/tests/hikari/impl/test_shard.py
@@ -789,6 +789,7 @@ def test__serialize_datetime_when_datetime_is_not_None(self, client):
 @pytest.mark.asyncio()
 class TestGatewayShardImplAsync:
     async def test_close_when_closing_event_set(self, client):
+        client._check_if_alive = mock.Mock()
         client._closing_event = mock.Mock(is_set=mock.Mock(return_value=True))
         client._closed_event = mock.Mock(wait=mock.AsyncMock())
         client._send_close = mock.Mock()
@@ -804,6 +805,7 @@ async def test_close_when_closing_event_set(self, client):
         client._closed_event.wait.assert_awaited_once_with()
 
     async def test_close_when_closing_event_not_set(self, client):
+        client._check_if_alive = mock.Mock()
         client._closing_event = mock.Mock(is_set=mock.Mock(return_value=False))
         client._closed_event = mock.Mock(wait=mock.AsyncMock())
         client._ws = mock.Mock(send_close=mock.AsyncMock())
@@ -821,6 +823,7 @@ async def test_close_when_closing_event_not_set(self, client):
         client._closed_event.wait.assert_awaited_once_with()
 
     async def test_close_when_closing_event_not_set_and_ws_is_None(self, client):
+        client._check_if_alive = mock.Mock()
         client._closing_event = mock.Mock(is_set=mock.Mock(return_value=False))
         client._closed_event = mock.Mock(wait=mock.AsyncMock())
         client._ws = None
@@ -846,6 +849,7 @@ async def test_when__user_id_is_not_None(self, client):
         assert await client.get_user_id() == 123
 
     async def test_join(self, client):
+        client._check_if_alive = mock.Mock()
         client._closed_event = mock.Mock(wait=mock.AsyncMock())
 
         await client.join()
diff --git a/tests/hikari/impl/test_voice.py b/tests/hikari/impl/test_voice.py
index da718377..6fc88575 100644
--- a/tests/hikari/impl/test_voice.py
+++ b/tests/hikari/impl/test_voice.py
@@ -297,7 +297,6 @@ class StubError(Exception):
                     predicate=voice_client._init_server_update_predicate.return_value,
                 ),
             ],
-            any_order=False,
         )
         mock_app.cache.get_me.assert_called_once_with()
         voice_client._init_state_update_predicate.assert_called_once_with(123, mock_app.cache.get_me.return_value.id)
diff --git a/tests/hikari/internal/test_ux.py b/tests/hikari/internal/test_ux.py
index f3ff235a..401bebeb 100644
--- a/tests/hikari/internal/test_ux.py
+++ b/tests/hikari/internal/test_ux.py
@@ -250,6 +250,61 @@ def test_when_doesnt_supports_color(self, mock_args):
         abspath.assert_called_once_with(dirname())
         supports_color.assert_called_once_with(True, False)
 
+    def test_use_extra_args(self, mock_args):
+        stack = contextlib.ExitStack()
+        stack.enter_context(mock.patch.object(colorlog.escape_codes, "escape_codes", new={}))
+        read_text = stack.enter_context(mock.patch.object(importlib.resources, "read_text"))
+        template = stack.enter_context(mock.patch.object(string, "Template"))
+        write = stack.enter_context(mock.patch.object(sys.stdout, "write"))
+        stack.enter_context(mock.patch.object(os.path, "abspath", return_value="some path"))
+
+        extra_args = {
+            "extra_argument_1": "one",
+            "extra_argument_2": "two",
+        }
+
+        with stack:
+            ux.print_banner("hikari", True, False, extra_args=extra_args)
+
+        args = {
+            # Hikari stuff.
+            "hikari_version": "2.2.2",
+            "hikari_git_sha1": "12345678",
+            "hikari_copyright": "© 2020 Nekokatt",
+            "hikari_license": "MIT",
+            "hikari_install_location": "some path",
+            "hikari_documentation_url": "https://nekokatt.github.io/hikari/docs",
+            "hikari_discord_invite": "https://discord.gg/Jx4cNGG",
+            "hikari_source_url": "https://nekokatt.github.io/hikari",
+            "python_implementation": "CPython",
+            "python_version": "4.0.0",
+            "system_description": "Machine Potato 1.0.0",
+        }
+
+        args.update(extra_args)
+
+        template.assert_called_once_with(read_text())
+        template().safe_substitute.assert_called_once_with(args)
+        write.assert_called_once_with(template().safe_substitute())
+
+    def test_overwrite_args_raises_error(self, mock_args):
+        stack = contextlib.ExitStack()
+        stack.enter_context(mock.patch.object(colorlog.escape_codes, "escape_codes", new={}))
+        stack.enter_context(mock.patch.object(importlib.resources, "read_text"))
+        stack.enter_context(mock.patch.object(string, "Template"))
+        stack.enter_context(mock.patch.object(sys.stdout, "write"))
+        stack.enter_context(mock.patch.object(os.path, "abspath", return_value="some path"))
+
+        extra_args = {
+            "hikari_version": "overwrite",
+        }
+
+        with stack:
+            with pytest.raises(
+                ValueError, match=r"Cannot overwrite \$-substitution `hikari_version`. Please use a different key."
+            ):
+                ux.print_banner("hikari", True, False, extra_args=extra_args)
+
 
 class TestSupportsColor:
     def test_when_not_allow_color(self):
diff --git a/tests/hikari/test_embeds.py b/tests/hikari/test_embeds.py
index 7faa731c..f7574e75 100644
--- a/tests/hikari/test_embeds.py
+++ b/tests/hikari/test_embeds.py
@@ -62,3 +62,49 @@ def test_proxy_filename(self, resource_with_proxy):
     def test_proxy_filename_when_resource_is_none(self, resource_with_proxy):
         resource_with_proxy.proxy_resource = None
         assert resource_with_proxy.proxy_filename is None
+
+
+class TestEmbed:
+    def test_total_length_when_embed_is_empty(self):
+        embed = embeds.Embed()
+        assert embed.total_length() == 0
+
+    def test_total_length_when_title_is_none(self):
+        embed = embeds.Embed(title=None)
+        assert embed.total_length() == 0
+
+    def test_total_length_title(self):
+        embed = embeds.Embed(title="title")
+        assert embed.total_length() == 5
+
+    def test_total_length_when_description_is_none(self):
+        embed = embeds.Embed(description=None)
+        assert embed.total_length() == 0
+
+    def test_total_length_description(self):
+        embed = embeds.Embed(description="description")
+        assert embed.total_length() == 11
+
+    def test_total_length_author_name(self):
+        embed = embeds.Embed().set_author(name="author name")
+        assert embed.total_length() == 11
+
+    def test_total_length_footer_text(self):
+        embed = embeds.Embed().set_footer(text="footer text")
+        assert embed.total_length() == 11
+
+    def test_total_length_field_name(self):
+        embed = embeds.Embed().add_field(name="field name", value="")
+        assert embed.total_length() == 10
+
+    def test_total_length_field_value(self):
+        embed = embeds.Embed().add_field(name="", value="field value")
+        assert embed.total_length() == 11
+
+    def test_total_length_all(self):
+        embed = embeds.Embed(title="title", description="description")
+        embed.set_author(name="author name")
+        embed.set_footer(text="footer text")
+        embed.add_field(name="field name 1", value="field value 2")
+        embed.add_field(name="field name 3", value="field value 4")
+        assert embed.total_length() == 88
diff --git a/tests/hikari/test_files.py b/tests/hikari/test_files.py
index 6084e257..59ee0c04 100644
--- a/tests/hikari/test_files.py
+++ b/tests/hikari/test_files.py
@@ -20,6 +20,15 @@
 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 # SOFTWARE.
 
+import base64
+import concurrent.futures
+import contextlib
+import pathlib
+import random
+import tempfile
+import typing
+
+import mock
 import pytest
 
 from hikari import files
@@ -41,3 +50,71 @@ def test___exit__(self, reader):
             reader().__exit__(None, None, None)
         except AttributeError as exc:
             pytest.fail(exc)
+
+
+class Test_FileAsyncReaderContextManagerImpl:
+    @pytest.mark.parametrize(
+        "executor", [concurrent.futures.ThreadPoolExecutor, concurrent.futures.ProcessPoolExecutor]
+    )
+    @pytest.mark.asyncio()
+    async def test_context_manager(self, executor: typing.Callable[[], concurrent.futures.Executor]):
+        mock_reader = mock.Mock(executor=executor())
+        context_manager = files._FileAsyncReaderContextManagerImpl(mock_reader)
+
+        with tempfile.NamedTemporaryFile() as file:
+            mock_reader.path = pathlib.Path(file.name)
+
+            async with context_manager as reader:
+                assert reader is mock_reader
+
+    @pytest.mark.asyncio()
+    async def test_context_manager_when_expandname_raises_runtime_error(self):
+        # We can't mock patch stuff in other processes easily (if at all) so
+        # for this test we only run it threaded.
+        mock_reader = mock.Mock(executor=concurrent.futures.ThreadPoolExecutor())
+        context_manager = files._FileAsyncReaderContextManagerImpl(mock_reader)
+
+        stack = contextlib.ExitStack()
+        file = stack.enter_context(tempfile.NamedTemporaryFile())
+        expandname = stack.enter_context(mock.patch.object(pathlib.Path, "expanduser", side_effect=RuntimeError))
+
+        with file:
+            mock_reader.path = pathlib.Path(file.name)
+
+            async with context_manager as reader:
+                assert reader is mock_reader
+
+        expandname.assert_called_once_with()
+
+    @pytest.mark.parametrize(
+        "executor", [concurrent.futures.ThreadPoolExecutor, concurrent.futures.ProcessPoolExecutor]
+    )
+    @pytest.mark.asyncio()
+    async def test_context_manager_for_unknown_file(self, executor: typing.Callable[[], concurrent.futures.Executor]):
+        mock_reader = mock.Mock(executor=executor())
+        context_manager = files._FileAsyncReaderContextManagerImpl(mock_reader)
+
+        mock_reader.path = pathlib.Path(
+            base64.urlsafe_b64encode(random.getrandbits(512).to_bytes(64, "little")).decode()
+        )
+
+        with pytest.raises(FileNotFoundError):  # noqa:  PT012 - raises block should contain a single statement
+            async with context_manager:
+                ...
+
+    @pytest.mark.parametrize(
+        "executor", [concurrent.futures.ThreadPoolExecutor, concurrent.futures.ProcessPoolExecutor]
+    )
+    @pytest.mark.asyncio()
+    async def test_test_context_manager_when_target_is_dir(
+        self, executor: typing.Callable[[], concurrent.futures.Executor]
+    ):
+        mock_reader = mock.Mock(executor=executor())
+        context_manager = files._FileAsyncReaderContextManagerImpl(mock_reader)
+
+        with tempfile.TemporaryDirectory() as name:
+            mock_reader.path = pathlib.Path(name)
+
+            with pytest.raises(IsADirectoryError):  # noqa:  PT012 - raises block should contain a single statement
+                async with context_manager:
+                    ...
diff --git a/tests/hikari/test_guilds.py b/tests/hikari/test_guilds.py
index ec90efdd..3caf43cc 100644
--- a/tests/hikari/test_guilds.py
+++ b/tests/hikari/test_guilds.py
@@ -296,7 +296,7 @@ def test_communication_disabled_until_when_raw_communication_disabled_until_is_N
         with mock.patch.object(time, "utc_datetime", return_value=datetime.datetime(2021, 10, 18)):
             assert model.communication_disabled_until() is None
 
-    def test_comminucation_disabled_until_when_raw_communication_disabled_until_is_in_the_past(self, model):
+    def test_communication_disabled_until_when_raw_communication_disabled_until_is_in_the_past(self, model):
         model.raw_communication_disabled_until = datetime.datetime(2021, 10, 18)
 
         with mock.patch.object(time, "utc_datetime", return_value=datetime.datetime(2021, 11, 22)):
@@ -469,6 +469,26 @@ def test_mention_property_when_no_nickname(self, model, mock_user):
         model.nickname = None
         assert model.mention == mock_user.mention
 
+    def test_get_guild(self, model):
+        guild = mock.Mock(id=456)
+        model.user.app.cache.get_guild.side_effect = [guild]
+
+        assert model.get_guild() == guild
+
+        model.user.app.cache.get_guild.assert_has_calls([mock.call(456)])
+
+    def test_get_guild_when_guild_not_in_cache(self, model):
+        model.user.app.cache.get_guild.side_effect = [None]
+
+        assert model.get_guild() is None
+
+        model.user.app.cache.get_guild.assert_has_calls([mock.call(456)])
+
+    def test_get_guild_when_no_cache_trait(self, model):
+        model.user.app = object()
+
+        assert model.get_guild() is None
+
     def test_get_roles(self, model):
         role1 = mock.Mock(id=321, position=2)
         role2 = mock.Mock(id=654, position=1)