Repository: hikari-py/hikari
Pull request #1213 (2022-07-15 07:08:38)

Improve BadRequestError error string
### 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.



Patch:
diff --git a/changes/1213.bugfix.md b/changes/1213.bugfix.md
new file mode 100644
index 00000000..ebb41cf5
--- /dev/null
+++ b/changes/1213.bugfix.md
@@ -0,0 +1,1 @@
+Improve `BadRequestError`'s error string.
diff --git a/changes/1266.bugfix.md b/changes/1266.bugfix.md
new file mode 100644
index 00000000..e63c56ff
--- /dev/null
+++ b/changes/1266.bugfix.md
@@ -0,0 +1,1 @@
+Allow for `replace_attachments` kwarg to be used in `RESTClient.create_initial_response`.
diff --git a/changes/1273.breaking.md b/changes/1273.breaking.md
new file mode 100644
index 00000000..ae6f812a
--- /dev/null
+++ b/changes/1273.breaking.md
@@ -0,0 +1,3 @@
+Remove support for ProcessPoolExecutor executor when reading files
+- It is much more efficient to use a threadpool executor for I/O actions like this one
+  - Due to the nature of process pool, we were also not able to perform proper chunking when reading off the file
diff --git a/changes/1284.bugfix.md b/changes/1284.bugfix.md
new file mode 100644
index 00000000..6b8986de
--- /dev/null
+++ b/changes/1284.bugfix.md
@@ -0,0 +1,1 @@
+Ignore guild create events which contain unavailable guilds
diff --git a/dev-requirements/build.txt b/dev-requirements/build.txt
index df9bce98..0f8168c8 100644
--- a/dev-requirements/build.txt
+++ b/dev-requirements/build.txt
@@ -1,2 +1,2 @@
-setuptools~=65.1.0
+setuptools>=65.2.0
 wheel==0.37.1
diff --git a/dev-requirements/flake8.txt b/dev-requirements/flake8.txt
index d375c105..8867e258 100644
--- a/dev-requirements/flake8.txt
+++ b/dev-requirements/flake8.txt
@@ -1,9 +1,9 @@
-flake8==4.0.1
+flake8==5.0.4
 
 # Plugins
 # Ref: https://github.com/DmytroLitvinov/awesome-flake8-extensions
 
-flake8-bandit~=3.0.0                    # runs bandit
+flake8-bandit~=4.1.1                    # runs bandit
 flake8-black==0.3.3                     # runs black
 flake8-broken-line==0.5.0               # forbey "\" linebreaks
 flake8-builtins==1.5.3                  # builtin shadowing checks
@@ -15,7 +15,6 @@ flake8-executable==2.1.1                # shebangs
 flake8-fixme==1.1.1                     # "fix me" counter
 flake8-functions==0.0.7                 # function linting
 flake8-html==0.4.2                      # html output
-flake8-if-statements==0.1.0             # condition linting
 flake8-isort==4.2.0                     # runs isort
 flake8-mutable==1.2.0                   # mutable default argument detection
 flake8-pep3101==1.3.0                   # new-style format strings only
diff --git a/dev-requirements/formatting.txt b/dev-requirements/formatting.txt
index 354a2a0d..ddf7fd7c 100644
--- a/dev-requirements/formatting.txt
+++ b/dev-requirements/formatting.txt
@@ -1,2 +1,2 @@
-black==22.6.0
+black==22.8.0
 isort==5.10.1
diff --git a/dev-requirements/pyright.txt b/dev-requirements/pyright.txt
index 84ecfde9..cabc77c7 100644
--- a/dev-requirements/pyright.txt
+++ b/dev-requirements/pyright.txt
@@ -1,1 +1,1 @@
-pyright==1.1.267
+pyright==1.1.271
diff --git a/dev-requirements/safety.txt b/dev-requirements/safety.txt
index c4d6f390..de1f816a 100644
--- a/dev-requirements/safety.txt
+++ b/dev-requirements/safety.txt
@@ -1,4 +1,4 @@
-safety~=2.1.1
+safety~=2.2.0
 
 # Temporary addition to avoid safety erroring due to https://github.com/pypa/pip/pull/9827
 pip>=21.1
diff --git a/dev-requirements/towncrier.txt b/dev-requirements/towncrier.txt
index 6c31b0cf..6928c85b 100644
--- a/dev-requirements/towncrier.txt
+++ b/dev-requirements/towncrier.txt
@@ -1,1 +1,1 @@
-towncrier==21.9.0
+towncrier==22.8.0
diff --git a/hikari/__init__.py b/hikari/__init__.py
index 674a9a4f..357cf847 100644
--- a/hikari/__init__.py
+++ b/hikari/__init__.py
@@ -41,11 +41,14 @@
 from hikari._about import __author__
 from hikari._about import __ci__
 from hikari._about import __copyright__
+from hikari._about import __coverage__
 from hikari._about import __discord_invite__
 from hikari._about import __docs__
 from hikari._about import __email__
+from hikari._about import __git_sha1__
 from hikari._about import __issue_tracker__
 from hikari._about import __license__
+from hikari._about import __maintainer__
 from hikari._about import __url__
 from hikari._about import __version__
 from hikari.applications import Application
diff --git a/hikari/__init__.pyi b/hikari/__init__.pyi
index 8f2b2839..252cf7f8 100644
--- a/hikari/__init__.pyi
+++ b/hikari/__init__.pyi
@@ -14,11 +14,14 @@ 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__
+from hikari._about import __coverage__ as __coverage__
 from hikari._about import __discord_invite__ as __discord_invite__
 from hikari._about import __docs__ as __docs__
 from hikari._about import __email__ as __email__
+from hikari._about import __git_sha1__ as __git_sha1__
 from hikari._about import __issue_tracker__ as __issue_tracker__
 from hikari._about import __license__ as __license__
+from hikari._about import __maintainer__ as __maintainer__
 from hikari._about import __url__ as __url__
 from hikari._about import __version__ as __version__
 from hikari.applications import Application as Application
diff --git a/hikari/api/rest.py b/hikari/api/rest.py
index 1221dd8f..26d154e8 100644
--- a/hikari/api/rest.py
+++ b/hikari/api/rest.py
@@ -7411,6 +7411,7 @@ async def create_interaction_response(
         components: undefined.UndefinedOr[typing.Sequence[special_endpoints.ComponentBuilder]] = undefined.UNDEFINED,
         embed: undefined.UndefinedOr[embeds_.Embed] = undefined.UNDEFINED,
         embeds: undefined.UndefinedOr[typing.Sequence[embeds_.Embed]] = undefined.UNDEFINED,
+        replace_attachments: bool = False,
         mentions_everyone: undefined.UndefinedOr[bool] = undefined.UNDEFINED,
         user_mentions: undefined.UndefinedOr[
             typing.Union[snowflakes.SnowflakeishSequence[users.PartialUser], bool]
@@ -7463,6 +7464,11 @@ async def create_interaction_response(
             If provided, the message embed.
         embeds : hikari.undefined.UndefinedOr[typing.Sequence[hikari.embeds.Embed]]
             If provided, the message embeds.
+        replace_attachments: bool
+            Whether to replace the attachments with the provided ones. Defaults
+            to `builtins.False`. This only effects component interactions.
+
+            Note this will also overwrite the embed attachments.
         flags : typing.Union[builtins.int, hikari.messages.MessageFlag, hikari.undefined.UndefinedType]
             If provided, the message flags this response should have.
 
diff --git a/hikari/commands.py b/hikari/commands.py
index 2909d6f2..2684a470 100644
--- a/hikari/commands.py
+++ b/hikari/commands.py
@@ -489,7 +489,7 @@ class SlashCommand(PartialCommand):
 @attr_extensions.with_copy
 @attr.define(hash=True, kw_only=True, weakref_slot=False)
 class ContextMenuCommand(PartialCommand):
-    """Represents a slash command on Discord."""
+    """Represents a context menu command on Discord."""
 
 
 class CommandPermissionType(int, enums.Enum):
diff --git a/hikari/errors.py b/hikari/errors.py
index ce5c9f8b..545b2490 100644
--- a/hikari/errors.py
+++ b/hikari/errors.py
@@ -285,6 +285,25 @@ class ClientHTTPResponseError(HTTPResponseError):
     """
 
 
+def _dump_errors(obj: data_binding.JSONObject, obj_string: str = "") -> str:
+    string = ""
+    for key, value in obj.items():
+        if isinstance(value, typing.Sequence):
+            string += obj_string + ":"
+
+            for item in value:
+                string += f"\n - {item['message']}"
+
+            string += "\n\n"
+
+            continue
+
+        current_obj_string = f"{obj_string}{'.' if obj_string else ''}{key}"
+        string += _dump_errors(value, current_obj_string)
+
+    return string
+
+
 @attr.define(auto_exc=True, repr=False, slots=False)
 class BadRequestError(ClientHTTPResponseError):
     """Raised when you send an invalid request somehow."""
@@ -308,7 +327,12 @@ def __str__(self) -> str:
 
         value = super().__str__()
         if self.errors:
-            value += "\n" + data_binding.dump_json(self.errors, indent=2)
+            value += "\n\n"
+
+            try:
+                value += _dump_errors(self.errors).strip("\n")
+            except KeyError:
+                value += data_binding.dump_json(self.errors, indent=2)
 
         self._cached_str = value
         return value
diff --git a/hikari/files.py b/hikari/files.py
index 80b896e8..aec1d811 100644
--- a/hikari/files.py
+++ b/hikari/files.py
@@ -36,7 +36,6 @@
     "AsyncReaderContextManager",
     "Resource",
     "File",
-    "FileReader",
     "WebResource",
     "URL",
     "WebReader",
@@ -48,13 +47,11 @@
 import asyncio
 import base64
 import concurrent.futures
-import errno
 import inspect
 import io
 import mimetypes
 import os
 import pathlib
-import stat
 import typing
 import urllib.parse
 import urllib.request
@@ -757,71 +754,7 @@ def filename(self) -> str:
 
 
 @attr.define(weakref_slot=False)
-class FileReader(AsyncReader, abc.ABC):
-    """Abstract base for a file reader object.
-
-    Various implementations have to exist in order to cater for situations
-    where we cannot pass IO objects around (e.g. ProcessPoolExecutors, since
-    they pickle things).
-    """
-
-    executor: typing.Optional[concurrent.futures.Executor] = attr.field()
-    """The associated `concurrent.futures.Executor` to use for blocking IO."""
-
-    path: pathlib.Path = attr.field(converter=ensure_path)
-    """The path to the resource to read."""
-
-
-def _stat(path: pathlib.Path) -> pathlib.Path:
-    # 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.
-
-    # path.stat() will raise FileNotFoundError if the file doesn't exist
-    # (unlike is_dir) which is what we want here.
-    if stat.S_ISDIR(path.stat().st_mode):
-        raise IsADirectoryError(errno.EISDIR, "Cannot open the path specified as it is a directory", str(path))
-
-    return path
-
-
-@attr.define(weakref_slot=False)
-@typing.final
-class _ThreadedFileReaderContextManagerImpl(AsyncReaderContextManager[FileReader]):
-    executor: typing.Optional[concurrent.futures.ThreadPoolExecutor] = attr.field()
-    file: typing.Optional[typing.BinaryIO] = attr.field(default=None, init=False)
-    filename: str = attr.field()
-    path: pathlib.Path = attr.field()
-
-    async def __aenter__(self) -> ThreadedFileReader:
-        if self.file:
-            raise RuntimeError("File is already open")
-
-        loop = asyncio.get_running_loop()
-        self.path = await loop.run_in_executor(self.executor, _stat, self.path)
-        self.file = typing.cast(io.BufferedReader, await loop.run_in_executor(self.executor, self.path.open, "rb"))
-        return ThreadedFileReader(self.filename, None, self.executor, self.path, self.file)
-
-    async def __aexit__(
-        self,
-        exc_type: typing.Optional[typing.Type[BaseException]],
-        exc: typing.Optional[BaseException],
-        exc_tb: typing.Optional[types.TracebackType],
-    ) -> None:
-        if not self.file:
-            raise RuntimeError("File isn't open")
-
-        loop = asyncio.get_running_loop()
-        file = self.file
-        self.file = None
-        await loop.run_in_executor(self.executor, file.close)
-
-
-@attr.define(weakref_slot=False)
-class ThreadedFileReader(FileReader):
+class ThreadedFileReader(AsyncReader):
     """Asynchronous file reader that reads a resource from local storage.
 
     This implementation works with pools that exist in the same interpreter
@@ -829,31 +762,39 @@ class ThreadedFileReader(FileReader):
     do not need to be pickled to be communicated.
     """
 
+    _executor: typing.Optional[concurrent.futures.ThreadPoolExecutor] = attr.field()
     _pointer: typing.BinaryIO = attr.field()
 
     async def __aiter__(self) -> typing.AsyncGenerator[typing.Any, bytes]:
         loop = asyncio.get_running_loop()
 
         while True:
-            chunk = await loop.run_in_executor(self.executor, self._pointer.read, _MAGIC)
+            chunk = await loop.run_in_executor(self._executor, self._pointer.read, _MAGIC)
             yield chunk
             if len(chunk) < _MAGIC:
                 break
 
 
+def _open_path(path: pathlib.Path) -> typing.BinaryIO:
+    return path.expanduser().open("rb")
+
+
 @attr.define(weakref_slot=False)
 @typing.final
-class _MultiProcessingFileReaderContextManagerImpl(AsyncReaderContextManager[FileReader]):
-    executor: concurrent.futures.ProcessPoolExecutor = attr.field()
+class _ThreadedFileReaderContextManagerImpl(AsyncReaderContextManager[ThreadedFileReader]):
+    executor: typing.Optional[concurrent.futures.ThreadPoolExecutor] = attr.field()
     file: typing.Optional[typing.BinaryIO] = attr.field(default=None, init=False)
     filename: str = attr.field()
     path: pathlib.Path = attr.field()
 
-    async def __aenter__(self) -> MultiprocessingFileReader:
-        loop = asyncio.get_running_loop()
+    async def __aenter__(self) -> ThreadedFileReader:
+        if self.file:
+            raise RuntimeError("File is already open")
 
-        path = await loop.run_in_executor(self.executor, _stat, self.path)
-        return MultiprocessingFileReader(self.filename, None, self.executor, path)
+        loop = asyncio.get_running_loop()
+        file = await loop.run_in_executor(self.executor, _open_path, self.path)
+        self.file = file
+        return ThreadedFileReader(self.filename, None, self.executor, file)
 
     async def __aexit__(
         self,
@@ -861,38 +802,15 @@ async def __aexit__(
         exc: typing.Optional[BaseException],
         exc_tb: typing.Optional[types.TracebackType],
     ) -> None:
-        pass
-
-
-def _read_all(path: pathlib.Path) -> bytes:
-    with path.open("rb") as file:
-        return file.read()
-
-
-@attr.define(slots=False, weakref_slot=False)  # Do not slot (pickle)
-class MultiprocessingFileReader(FileReader):
-    """Asynchronous file reader that reads a resource from local storage.
-
-    This implementation works with pools that exist in a different interpreter
-    instance to the caller. Currently this only includes ProcessPoolExecutors
-    and custom implementations where objects have to be pickled to be used
-    by the pool.
-    """
-
-    async def __aiter__(self) -> typing.AsyncGenerator[typing.Any, bytes]:
-        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}
+        if not self.file:
+            raise RuntimeError("File isn't open")
 
-    def __setstate__(self, state: typing.Dict[str, typing.Any]) -> None:
-        self.path: pathlib.Path = state["path"]
-        self.filename: str = state["filename"]
-        self.executor: typing.Optional[concurrent.futures.Executor] = None
-        self.mimetype: typing.Optional[str] = None
+        loop = asyncio.get_running_loop()
+        await loop.run_in_executor(self.executor, self.file.close)
+        self.file = None
 
 
-class File(Resource[FileReader]):
+class File(Resource[ThreadedFileReader]):
     """A resource that exists on the local machine's storage to be uploaded.
 
     Parameters
@@ -948,21 +866,23 @@ def stream(
         *,
         executor: typing.Optional[concurrent.futures.Executor] = None,
         head_only: bool = False,
-    ) -> AsyncReaderContextManager[FileReader]:
+    ) -> AsyncReaderContextManager[ThreadedFileReader]:
         """Start streaming the resource using a thread pool executor.
 
         Parameters
         ----------
         executor : typing.Optional[concurrent.futures.Executor]
-            The executor to run the blocking read operations in. If
+            The thread executor to run the blocking read operations in. If
             `builtins.None`, the default executor for the running event loop
             will be used instead.
+
+            Only `concurrent.futures.TheadPoolExecutor` is supported.
         head_only : builtins.bool
             Not used. Provided only to match the underlying interface.
 
         Returns
         -------
-        AsyncReaderContextManager[FileReader]
+        AsyncReaderContextManager[ThreadedFileReader]
             An async context manager that when entered, produces the
             data stream.
 
@@ -973,15 +893,12 @@ def stream(
         FileNotFoundError
             If the file doesn't exist.
         """
-        # asyncio forces the default executor when this is None to always be a thread pool executor anyway,
-        # so this is safe enough to do.:
         if executor is None or isinstance(executor, concurrent.futures.ThreadPoolExecutor):
+            # asyncio forces the default executor when this is None to always be a thread pool executor anyway,
+            # so this is safe enough to do:
             return _ThreadedFileReaderContextManagerImpl(executor, self.filename, self.path)
 
-        if not isinstance(executor, concurrent.futures.ProcessPoolExecutor):
-            raise TypeError("The executor must be a ProcessPoolExecutor, ThreadPoolExecutor, or `builtins.None`.")
-
-        return _MultiProcessingFileReaderContextManagerImpl(executor, self.filename, self.path)
+        raise TypeError("The executor must be a ThreadPoolExecutor or None")
 
 
 ########################################################################
diff --git a/hikari/impl/event_manager.py b/hikari/impl/event_manager.py
index 0af15177..4e159f85 100644
--- a/hikari/impl/event_manager.py
+++ b/hikari/impl/event_manager.py
@@ -180,9 +180,17 @@ async def on_guild_create(  # noqa: C901 - Function too complex
         """See https://discord.com/developers/docs/topics/gateway#guild-create for more info."""
         event: typing.Union[guild_events.GuildAvailableEvent, guild_events.GuildJoinEvent, None]
 
-        if "unavailable" in payload and self._enabled_for_event(guild_events.GuildAvailableEvent):
+        unavailable = payload.get("unavailable")
+
+        if unavailable:
+            # It is possible for GUILD_CREATE to contain unavailable guilds during outages
+            # In cases like this, we can just ignore the event and wait for the outage to
+            # be resolved, where the correct guild visibility event will be dispatched
+            return
+
+        if unavailable is not None and self._enabled_for_event(guild_events.GuildAvailableEvent):
             event = self._event_factory.deserialize_guild_available_event(shard, payload)
-        elif "unavailable" not in payload and self._enabled_for_event(guild_events.GuildJoinEvent):
+        elif unavailable is None and self._enabled_for_event(guild_events.GuildJoinEvent):
             event = self._event_factory.deserialize_guild_join_event(shard, payload)
         else:
             event = None
diff --git a/hikari/impl/rest.py b/hikari/impl/rest.py
index 054de53b..d1a255f0 100644
--- a/hikari/impl/rest.py
+++ b/hikari/impl/rest.py
@@ -3444,6 +3444,7 @@ async def create_interaction_response(
         components: undefined.UndefinedOr[typing.Sequence[special_endpoints.ComponentBuilder]] = undefined.UNDEFINED,
         embed: undefined.UndefinedOr[embeds_.Embed] = undefined.UNDEFINED,
         embeds: undefined.UndefinedOr[typing.Sequence[embeds_.Embed]] = undefined.UNDEFINED,
+        replace_attachments: bool = False,
         mentions_everyone: undefined.UndefinedOr[bool] = undefined.UNDEFINED,
         user_mentions: undefined.UndefinedOr[
             typing.Union[snowflakes.SnowflakeishSequence[users.PartialUser], bool]
@@ -3462,6 +3463,7 @@ async def create_interaction_response(
             components=components,
             embed=embed,
             embeds=embeds,
+            replace_attachments=replace_attachments,
             tts=tts,
             flags=flags,
             mentions_everyone=mentions_everyone,
diff --git a/hikari/impl/special_endpoints.py b/hikari/impl/special_endpoints.py
index b4d0c3cc..6cb801b2 100644
--- a/hikari/impl/special_endpoints.py
+++ b/hikari/impl/special_endpoints.py
@@ -1258,6 +1258,7 @@ async def create(
             self._type,
             self._name,
             guild=guild,
+            name_localizations=self._name_localizations,
             default_member_permissions=self._default_member_permissions,
             dm_enabled=self._is_dm_enabled,
         )
diff --git a/hikari/interactions/base_interactions.py b/hikari/interactions/base_interactions.py
index 08019661..6eb22fd4 100644
--- a/hikari/interactions/base_interactions.py
+++ b/hikari/interactions/base_interactions.py
@@ -249,6 +249,7 @@ async def create_initial_response(
         components: undefined.UndefinedOr[typing.Sequence[special_endpoints.ComponentBuilder]] = undefined.UNDEFINED,
         embed: undefined.UndefinedOr[embeds_.Embed] = undefined.UNDEFINED,
         embeds: undefined.UndefinedOr[typing.Sequence[embeds_.Embed]] = undefined.UNDEFINED,
+        replace_attachments: bool = False,
         mentions_everyone: undefined.UndefinedOr[bool] = undefined.UNDEFINED,
         user_mentions: undefined.UndefinedOr[
             typing.Union[snowflakes.SnowflakeishSequence[users.PartialUser], bool]
@@ -296,6 +297,11 @@ async def create_initial_response(
             If provided, the message embed.
         embeds : hikari.undefined.UndefinedOr[typing.Sequence[hikari.embeds.Embed]]
             If provided, the message embeds.
+        replace_attachments: bool
+            Whether to replace the attachments with the provided ones. Defaults
+            to `builtins.False`. This only effects component interactions.
+
+            Note this will also overwrite the embed attachments.
         flags : typing.Union[builtins.int, hikari.messages.MessageFlag, hikari.undefined.UndefinedType]
             If provided, the message flags this response should have.
 
@@ -367,6 +373,7 @@ async def create_initial_response(
             components=components,
             embed=embed,
             embeds=embeds,
+            replace_attachments=replace_attachments,
             flags=flags,
             mentions_everyone=mentions_everyone,
             user_mentions=user_mentions,
diff --git a/pipelines/format.nox.py b/pipelines/format.nox.py
index 643c7ea3..81672dc3 100644
--- a/pipelines/format.nox.py
+++ b/pipelines/format.nox.py
@@ -35,13 +35,12 @@
 @nox.session(reuse_venv=True)
 def reformat_code(session: nox.Session) -> None:
     """Remove trailing whitespace in source, run isort, codespell and then run black code formatter."""
-    session.install(*nox.dev_requirements("formatting", "codespell"))
+    session.install(*nox.dev_requirements("formatting"))
 
     remove_trailing_whitespaces(session)
 
     session.run("isort", *config.PYTHON_REFORMATTING_PATHS)
     session.run("black", *config.PYTHON_REFORMATTING_PATHS)
-    session.run("codespell", "-w", *config.FULL_REFORMATTING_PATHS)
 
 
 @nox.session(reuse_venv=True)
diff --git a/pyproject.toml b/pyproject.toml
index 33c33f6b..3cc73903 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -156,6 +156,5 @@ require-subclass = true
 exclude-classes = """
 (
     ^hikari\\.internal\\.enums:(Enum|Flag|_EnumMeta|_FlagMeta)$
-    |^hikari\\.files:MultiprocessingFileReader$
 )
 """
diff --git a/requirements.txt b/requirements.txt
index 82d4e349..8345feba 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,4 +1,4 @@
 aiohttp~=3.8
 attrs~=22.1
-colorlog~=6.6
+colorlog~=6.7
 multidict~=6.0


Test patch:
diff --git a/dev-requirements/pytest.txt b/dev-requirements/pytest.txt
index fff06958..015cb30b 100644
--- a/dev-requirements/pytest.txt
+++ b/dev-requirements/pytest.txt
@@ -1,7 +1,7 @@
 # mock in the stdlib changes between versions of Python, so are not consistent in behaviour like the backport is
 mock==4.0.3
 
-pytest==7.1.2
+pytest==7.1.3
 pytest-asyncio==0.19.0
 pytest-cov==3.0.0
 pytest-randomly==3.12.0
diff --git a/tests/hikari/impl/test_event_manager.py b/tests/hikari/impl/test_event_manager.py
index 1374433b..50839974 100644
--- a/tests/hikari/impl/test_event_manager.py
+++ b/tests/hikari/impl/test_event_manager.py
@@ -246,12 +246,46 @@ async def test_on_channel_pins_update(self, stateless_event_manager_impl, shard,
             event_factory.deserialize_channel_pins_update_event.return_value
         )
 
-    @pytest.mark.parametrize("unavailable", [True, False])
+    @pytest.mark.asyncio()
+    async def test_on_guild_create_when_unavailable_guild(
+        self, event_manager_impl, shard, event_factory, entity_factory
+    ):
+        payload = {"unavailable": True}
+        event_manager_impl._cache_enabled_for = mock.Mock(return_value=True)
+        event_manager_impl._enabled_for_event = mock.Mock(return_value=True)
+
+        with mock.patch.object(event_manager, "_request_guild_members") as request_guild_members:
+            await event_manager_impl.on_guild_create(shard, payload)
+
+        event_manager_impl._enabled_for_event.assert_not_called()
+        event_factory.deserialize_guild_available_event.assert_not_called()
+        event_factory.deserialize_guild_join_event.assert_not_called()
+
+        event_manager_impl._cache.update_guild.assert_not_called()
+        event_manager_impl._cache.clear_guild_channels_for_guild.assert_not_called()
+        event_manager_impl._cache.set_guild_channel.assert_not_called()
+        event_manager_impl._cache.clear_emojis_for_guild.assert_not_called()
+        event_manager_impl._cache.set_emoji.assert_not_called()
+        event_manager_impl._cache.clear_stickers_for_guild.assert_not_called()
+        event_manager_impl._cache.set_sticker.assert_not_called()
+        event_manager_impl._cache.clear_roles_for_guild.assert_not_called()
+        event_manager_impl._cache.set_role.assert_not_called()
+        event_manager_impl._cache.clear_members_for_guild.assert_not_called()
+        event_manager_impl._cache.set_member.assert_not_called()
+        event_manager_impl._cache.clear_presences_for_guild.assert_not_called()
+        event_manager_impl._cache.set_presence.assert_not_called()
+        event_manager_impl._cache.clear_voice_states_for_guild.assert_not_called()
+        event_manager_impl._cache.set_voice_state.assert_not_called()
+        request_guild_members.assert_not_called()
+
+        event_manager_impl.dispatch.assert_not_called()
+
+    @pytest.mark.parametrize("include_unavailable", [True, False])
     @pytest.mark.asyncio()
     async def test_on_guild_create_when_dispatching_and_not_caching(
-        self, event_manager_impl, shard, event_factory, entity_factory, unavailable
+        self, event_manager_impl, shard, event_factory, entity_factory, include_unavailable
     ):
-        payload = {"unavailable": False} if unavailable else {}
+        payload = {"unavailable": False} if include_unavailable else {}
         event_manager_impl._intents = intents.Intents.NONE
         event_manager_impl._cache_enabled_for = mock.Mock(return_value=False)
         event_manager_impl._enabled_for_event = mock.Mock(return_value=True)
@@ -259,7 +293,7 @@ async def test_on_guild_create_when_dispatching_and_not_caching(
         with mock.patch.object(event_manager, "_request_guild_members") as request_guild_members:
             await event_manager_impl.on_guild_create(shard, payload)
 
-        if unavailable:
+        if include_unavailable:
             event_manager_impl._enabled_for_event.assert_called_once_with(guild_events.GuildAvailableEvent)
             event_factory.deserialize_guild_available_event.assert_called_once_with(shard, payload)
             event = event_factory.deserialize_guild_available_event.return_value
@@ -287,12 +321,12 @@ async def test_on_guild_create_when_dispatching_and_not_caching(
 
         event_manager_impl.dispatch.assert_awaited_once_with(event)
 
-    @pytest.mark.parametrize("unavailable", [True, False])
+    @pytest.mark.parametrize("include_unavailable", [True, False])
     @pytest.mark.asyncio()
     async def test_on_guild_create_when_not_dispatching_and_not_caching(
-        self, event_manager_impl, shard, event_factory, entity_factory, unavailable
+        self, event_manager_impl, shard, event_factory, entity_factory, include_unavailable
     ):
-        payload = {"unavailable": False} if unavailable else {}
+        payload = {"unavailable": False} if include_unavailable else {}
         event_manager_impl._intents = intents.Intents.NONE
         event_manager_impl._cache_enabled_for = mock.Mock(return_value=False)
         event_manager_impl._enabled_for_event = mock.Mock(return_value=False)
@@ -300,7 +334,7 @@ async def test_on_guild_create_when_not_dispatching_and_not_caching(
         with mock.patch.object(event_manager, "_request_guild_members") as request_guild_members:
             await event_manager_impl.on_guild_create(shard, payload)
 
-        if unavailable:
+        if include_unavailable:
             event_manager_impl._enabled_for_event.assert_called_once_with(guild_events.GuildAvailableEvent)
         else:
             event_manager_impl._enabled_for_event.assert_called_once_with(guild_events.GuildJoinEvent)
@@ -327,12 +361,12 @@ async def test_on_guild_create_when_not_dispatching_and_not_caching(
 
         event_manager_impl.dispatch.assert_not_called()
 
-    @pytest.mark.parametrize("unavailable", [True, False])
+    @pytest.mark.parametrize("include_unavailable", [True, False])
     @pytest.mark.asyncio()
     async def test_on_guild_create_when_not_dispatching_and_caching(
-        self, event_manager_impl, shard, event_factory, entity_factory, unavailable
+        self, event_manager_impl, shard, event_factory, entity_factory, include_unavailable
     ):
-        payload = {"unavailable": False} if unavailable else {}
+        payload = {"unavailable": False} if include_unavailable else {}
         event_manager_impl._intents = intents.Intents.NONE
         event_manager_impl._cache_enabled_for = mock.Mock(return_value=True)
         event_manager_impl._enabled_for_event = mock.Mock(return_value=False)
@@ -348,7 +382,7 @@ async def test_on_guild_create_when_not_dispatching_and_caching(
         with mock.patch.object(event_manager, "_request_guild_members") as request_guild_members:
             await event_manager_impl.on_guild_create(shard, payload)
 
-        if unavailable:
+        if include_unavailable:
             event_manager_impl._enabled_for_event.assert_called_once_with(guild_events.GuildAvailableEvent)
         else:
             event_manager_impl._enabled_for_event.assert_called_once_with(guild_events.GuildJoinEvent)
@@ -375,13 +409,13 @@ async def test_on_guild_create_when_not_dispatching_and_caching(
 
         event_manager_impl.dispatch.assert_not_called()
 
-    @pytest.mark.parametrize("unavailable", [True, False])
+    @pytest.mark.parametrize("include_unavailable", [True, False])
     @pytest.mark.asyncio()
     async def test_on_guild_create_when_stateless(
-        self, stateless_event_manager_impl, shard, event_factory, entity_factory, unavailable
+        self, stateless_event_manager_impl, shard, event_factory, entity_factory, include_unavailable
     ):
         payload = {"id": 123}
-        if unavailable:
+        if include_unavailable:
             payload["unavailable"] = False
 
         stateless_event_manager_impl._intents = intents.Intents.NONE
@@ -391,7 +425,7 @@ async def test_on_guild_create_when_stateless(
         with mock.patch.object(event_manager, "_request_guild_members") as request_guild_members:
             await stateless_event_manager_impl.on_guild_create(shard, payload)
 
-        if unavailable:
+        if include_unavailable:
             stateless_event_manager_impl._enabled_for_event.assert_called_once_with(guild_events.GuildAvailableEvent)
         else:
             stateless_event_manager_impl._enabled_for_event.assert_called_once_with(guild_events.GuildJoinEvent)
diff --git a/tests/hikari/impl/test_rest.py b/tests/hikari/impl/test_rest.py
index 47febacb..4d7dd278 100644
--- a/tests/hikari/impl/test_rest.py
+++ b/tests/hikari/impl/test_rest.py
@@ -5099,6 +5099,7 @@ async def test_create_interaction_response_when_form(self, rest_client):
             components=[component_obj2],
             embed=embed_obj,
             embeds=[embed_obj2],
+            replace_attachments=True,
             tts=True,
             flags=120,
             mentions_everyone=False,
@@ -5114,6 +5115,7 @@ async def test_create_interaction_response_when_form(self, rest_client):
             components=[component_obj2],
             embed=embed_obj,
             embeds=[embed_obj2],
+            replace_attachments=True,
             tts=True,
             flags=120,
             mentions_everyone=False,
@@ -5149,6 +5151,7 @@ async def test_create_interaction_response_when_no_form(self, rest_client):
             components=[component_obj2],
             embed=embed_obj,
             embeds=[embed_obj2],
+            replace_attachments=True,
             tts=True,
             flags=120,
             mentions_everyone=False,
@@ -5164,6 +5167,7 @@ async def test_create_interaction_response_when_no_form(self, rest_client):
             components=[component_obj2],
             embed=embed_obj,
             embeds=[embed_obj2],
+            replace_attachments=True,
             tts=True,
             flags=120,
             mentions_everyone=False,
diff --git a/tests/hikari/impl/test_special_endpoints.py b/tests/hikari/impl/test_special_endpoints.py
index f9d8fadb..c1e1e1bc 100644
--- a/tests/hikari/impl/test_special_endpoints.py
+++ b/tests/hikari/impl/test_special_endpoints.py
@@ -808,6 +808,7 @@ async def test_create(self):
         builder = (
             special_endpoints.ContextMenuCommandBuilder(commands.CommandType.USER, "we are number")
             .set_default_member_permissions(permissions.Permissions.BAN_MEMBERS)
+            .set_name_localizations({"meow": "nyan"})
             .set_is_dm_enabled(True)
         )
         mock_rest = mock.AsyncMock()
@@ -821,6 +822,7 @@ async def test_create(self):
             builder.name,
             guild=undefined.UNDEFINED,
             default_member_permissions=permissions.Permissions.BAN_MEMBERS,
+            name_localizations={"meow": "nyan"},
             dm_enabled=True,
         )
 
@@ -829,6 +831,7 @@ async def test_create_with_guild(self):
         builder = (
             special_endpoints.ContextMenuCommandBuilder(commands.CommandType.USER, "we are number")
             .set_default_member_permissions(permissions.Permissions.BAN_MEMBERS)
+            .set_name_localizations({"en-ghibli": "meow"})
             .set_is_dm_enabled(True)
         )
         mock_rest = mock.AsyncMock()
@@ -842,6 +845,7 @@ async def test_create_with_guild(self):
             builder.name,
             guild=765234123,
             default_member_permissions=permissions.Permissions.BAN_MEMBERS,
+            name_localizations={"en-ghibli": "meow"},
             dm_enabled=True,
         )
 
diff --git a/tests/hikari/interactions/test_base_interactions.py b/tests/hikari/interactions/test_base_interactions.py
index 16f92862..364e58f9 100644
--- a/tests/hikari/interactions/test_base_interactions.py
+++ b/tests/hikari/interactions/test_base_interactions.py
@@ -87,6 +87,7 @@ async def test_create_initial_response_with_optional_args(self, mock_message_res
             components=mock_components,
             attachment=mock_attachment,
             attachments=mock_attachments,
+            replace_attachments=True,
             mentions_everyone=False,
             user_mentions=[123432],
             role_mentions=[6324523],
@@ -105,6 +106,7 @@ async def test_create_initial_response_with_optional_args(self, mock_message_res
             components=mock_components,
             attachment=mock_attachment,
             attachments=mock_attachments,
+            replace_attachments=True,
             mentions_everyone=False,
             user_mentions=[123432],
             role_mentions=[6324523],
@@ -129,6 +131,7 @@ async def test_create_initial_response_without_optional_args(self, mock_message_
             components=undefined.UNDEFINED,
             attachment=undefined.UNDEFINED,
             attachments=undefined.UNDEFINED,
+            replace_attachments=False,
             mentions_everyone=undefined.UNDEFINED,
             user_mentions=undefined.UNDEFINED,
             role_mentions=undefined.UNDEFINED,
diff --git a/tests/hikari/test_errors.py b/tests/hikari/test_errors.py
index 9b71a109..9809b198 100644
--- a/tests/hikari/test_errors.py
+++ b/tests/hikari/test_errors.py
@@ -21,6 +21,7 @@
 # SOFTWARE.
 
 import http
+import inspect
 
 import mock
 import pytest
@@ -108,28 +109,102 @@ class TestBadRequestError:
     @pytest.fixture()
     def error(self):
         return errors.BadRequestError(
-            "https://some.url", http.HTTPStatus.BAD_REQUEST, {}, "raw body", errors={"components": {"0": "ok"}}
+            "https://some.url",
+            http.HTTPStatus.BAD_REQUEST,
+            {},
+            "raw body",
+            errors={
+                "components": {
+                    "0": {
+                        "_errors": [
+                            {"code": "123", "message": "something went wrong"},
+                            {"code": "456", "message": "but more things too!"},
+                        ],
+                    },
+                },
+                "attachments": {
+                    "1": {
+                        "_errors": [
+                            {"code": "789", "message": "at this point, all wrong!"},
+                        ],
+                    },
+                },
+            },
         )
 
     def test_str(self, error):
-        assert (
-            str(error)
-            == 'Bad Request 400: \'raw body\' for https://some.url\n{\n  "components": {\n    "0": "ok"\n  }\n}'
+        with mock.patch.object(data_binding, "dump_json") as dump_json:
+            string = str(error)
+
+        assert string == inspect.cleandoc(
+            """
+            Bad Request 400: 'raw body' for https://some.url
+
+            components.0:
+             - something went wrong
+             - but more things too!
+
+            attachments.1:
+             - at this point, all wrong!
+            """
+        )
+        dump_json.assert_not_called()
+
+    def test_str_when_dump_error_errors(self, error):
+        with mock.patch.object(errors, "_dump_errors", side_effect=KeyError):
+            string = str(error)
+
+        assert string == inspect.cleandoc(
+            """
+            Bad Request 400: 'raw body' for https://some.url
+
+            {
+              "components": {
+                "0": {
+                  "_errors": [
+                    {
+                      "code": "123",
+                      "message": "something went wrong"
+                    },
+                    {
+                      "code": "456",
+                      "message": "but more things too!"
+                    }
+                  ]
+                }
+              },
+              "attachments": {
+                "1": {
+                  "_errors": [
+                    {
+                      "code": "789",
+                      "message": "at this point, all wrong!"
+                    }
+                  ]
+                }
+              }
+            }
+            """
         )
 
     def test_str_when_cached(self, error):
         error._cached_str = "ok"
 
         with mock.patch.object(data_binding, "dump_json") as dump_json:
-            assert str(error) == "ok"
+            with mock.patch.object(errors, "_dump_errors") as dump_errors:
+                assert str(error) == "ok"
 
+        dump_errors.assert_not_called()
         dump_json.assert_not_called()
 
     def test_str_when_no_errors(self, error):
         error.errors = None
+
         with mock.patch.object(data_binding, "dump_json") as dump_json:
-            assert str(error) == "Bad Request 400: 'raw body' for https://some.url"
+            with mock.patch.object(errors, "_dump_errors") as dump_errors:
+                assert str(error) == "Bad Request 400: 'raw body' for https://some.url"
 
+        dump_errors.assert_not_called()
         dump_json.assert_not_called()
 
 
diff --git a/tests/hikari/test_files.py b/tests/hikari/test_files.py
index 380db057..4660ae1c 100644
--- a/tests/hikari/test_files.py
+++ b/tests/hikari/test_files.py
@@ -20,11 +20,8 @@
 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 # SOFTWARE.
 
-import base64
-import concurrent.futures
+import asyncio
 import pathlib
-import random
-import tempfile
 
 import mock
 import pytest
@@ -50,7 +47,16 @@ def test___exit__(self, reader):
             pytest.fail(exc)
 
 
-class Test_ThreadedFileReaderContextManagerImpl:
+def test__open_path():
+    expanded_path = mock.Mock()
+    path = mock.Mock(expanduser=mock.Mock(return_value=expanded_path))
+
+    assert files._open_path(path) is expanded_path.open.return_value
+
+    expanded_path.open.assert_called_once_with("rb")
+
+
+class TestThreadedFileReaderContextManagerImpl:
     @pytest.mark.asyncio()
     async def test_enter_dunder_method_when_already_open(self):
         manager = files._ThreadedFileReaderContextManagerImpl(mock.Mock(), "ea", pathlib.Path("ea"))
@@ -67,138 +73,23 @@ async def test_exit_dunder_method_when_not_open(self):
 
     @pytest.mark.asyncio()
     async def test_context_manager(self):
-        executor = concurrent.futures.ThreadPoolExecutor()
-        mock_data = b"meeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" * 50
+        mock_file = mock.Mock()
+        executor = object()
+        path = pathlib.Path("test/path/")
 
-        # A try, finally is used to delete the file rather than relying on delete=True behaviour
-        # as on Windows the file cannot be accessed by other processes if delete is True.
-        file = tempfile.NamedTemporaryFile("wb", delete=False)
-        path = pathlib.Path(file.name)
-        try:
-            with file:
-                file.write(mock_data)
+        loop = mock.Mock(run_in_executor=mock.AsyncMock(side_effect=[mock_file, None]))
 
-            context_manager = files._ThreadedFileReaderContextManagerImpl(executor, "meow.txt", path)
+        context_manager = files._ThreadedFileReaderContextManagerImpl(executor, "meow.txt", path)
 
+        with mock.patch.object(asyncio, "get_running_loop", return_value=loop):
             async with context_manager as reader:
-                data = await reader.read()
+                assert context_manager.file is mock_file
 
                 assert reader.filename == "meow.txt"
-                assert reader.path == path
-                assert reader.executor is executor
-                assert data == mock_data
-
-        finally:
-            path.unlink()
-
-    @mock.patch.object(pathlib.Path, "expanduser", side_effect=RuntimeError)
-    @pytest.mark.asyncio()
-    async def test_context_manager_when_expandname_raises_runtime_error(self, expanduser: mock.Mock):
-        # We can't mock patch stuff in other processes easily (if at all) so
-        # for this test we have to cheat and use a thread pool executor.
-        executor = concurrent.futures.ThreadPoolExecutor()
-
-        # A try, finally is used to delete the file rather than relying on delete=True behaviour
-        # as on Windows the file cannot be accessed by other processes if delete is True.
-        with tempfile.NamedTemporaryFile(delete=False) as file:
-            pass
-
-        path = pathlib.Path(file.name)
-        try:
-            context_manager = files._ThreadedFileReaderContextManagerImpl(executor, "filename.txt", path)
-
-            async with context_manager as reader:
-                assert reader.path == path
-
-            expanduser.assert_called_once_with()
-
-        finally:
-            path.unlink()
-
-    @pytest.mark.asyncio()
-    async def test_context_manager_for_unknown_file(self):
-        executor = concurrent.futures.ThreadPoolExecutor()
-        path = pathlib.Path(base64.urlsafe_b64encode(random.getrandbits(512).to_bytes(64, "little")).decode())
-        context_manager = files._ThreadedFileReaderContextManagerImpl(executor, "ea.txt", path)
-
-        with pytest.raises(FileNotFoundError):  # noqa:  PT012 - raises block should contain a single statement
-            async with context_manager:
-                ...
-
-    @pytest.mark.asyncio()
-    async def test_test_context_manager_when_target_is_dir(self):
-        executor = concurrent.futures.ThreadPoolExecutor()
-
-        with tempfile.TemporaryDirectory() as name:
-            path = pathlib.Path(name)
-            context_manager = files._ThreadedFileReaderContextManagerImpl(executor, "meow.txt", path)
-
-            with pytest.raises(IsADirectoryError):  # noqa:  PT012 - raises block should contain a single statement
-                async with context_manager:
-                    ...
-
-
-class Test_MultiProcessingFileReaderContextManagerImpl:
-    @pytest.mark.asyncio()
-    async def test_context_manager(self):
-        executor = concurrent.futures.ProcessPoolExecutor()
-        mock_data = b"kon'nichiwa i am yellow and blue da be meow da bayeet" * 50
-
-        # A try, finally is used to delete the file rather than relying on delete=True behaviour
-        # as on Windows the file cannot be accessed by other processes if delete is True.
-        file = tempfile.NamedTemporaryFile("wb", delete=False)
-        path = pathlib.Path(file.name)
-        try:
-            with file:
-                file.write(mock_data)
-
-            context_manager = files._MultiProcessingFileReaderContextManagerImpl(executor, "filename.txt", path)
-
-            async with context_manager as reader:
-                data = await reader.read()
-
-                assert reader.filename == "filename.txt"
-                assert reader.path == path
-                assert reader.executor is executor
-                assert data == mock_data
-
-        finally:
-            path.unlink()
-
-    @mock.patch.object(pathlib.Path, "expanduser", side_effect=RuntimeError)
-    @pytest.mark.asyncio()
-    async def test_context_manager_when_expandname_raises_runtime_error(self, expanduser: mock.Mock):
-        # We can't mock patch stuff in other processes easily (if at all) so
-        # for this test we have to cheat and use a thread pool executor.
-        executor = concurrent.futures.ThreadPoolExecutor()
-
-        with tempfile.NamedTemporaryFile() as file:
-            path = pathlib.Path(file.name)
-            context_manager = files._MultiProcessingFileReaderContextManagerImpl(executor, "filename.txt", path)
-
-            async with context_manager as reader:
-                assert reader.path == path
-
-        expanduser.assert_called_once_with()
-
-    @pytest.mark.asyncio()
-    async def test_context_manager_for_unknown_file(self):
-        executor = concurrent.futures.ProcessPoolExecutor()
-        path = pathlib.Path(base64.urlsafe_b64encode(random.getrandbits(512).to_bytes(64, "little")).decode())
-        context_manager = files._MultiProcessingFileReaderContextManagerImpl(executor, "ea.txt", path)
-
-        with pytest.raises(FileNotFoundError):  # noqa:  PT012 - raises block should contain a single statement
-            async with context_manager:
-                ...
-
-    @pytest.mark.asyncio()
-    async def test_test_context_manager_when_target_is_dir(self):
-        executor = concurrent.futures.ProcessPoolExecutor()
+                assert reader._executor is executor
 
-        with tempfile.TemporaryDirectory() as name:
-            path = pathlib.Path(name)
-            context_manager = files._MultiProcessingFileReaderContextManagerImpl(executor, "meow.txt", path)
+                loop.run_in_executor.assert_called_once_with(executor, files._open_path, path)
+                loop.run_in_executor.reset_mock()
 
-            with pytest.raises(IsADirectoryError):  # noqa:  PT012 - raises block should contain a single statement
-                async with context_manager:
-                    ...
+        loop.run_in_executor.assert_called_once_with(executor, mock_file.close)
+        assert context_manager.file is None