Repository: hikari-py/hikari (python)

Issue:
Editing the icon of a server fails 
### Steps to reproduce

```py
await bot.rest.edit_guild(ctx.guild_id, icon="path on disk or url")
```


### Expected result

Server icon should have changed

### Actual result

```
Traceback (most recent call last):
  File "D:\Documents\Git Stuff\hikari-test-bot\.venv\Lib\site-packages\lightbulb\app.py", line 1162, in invoke_application_command
    await context.invoke()
  File "D:\Documents\Git Stuff\hikari-test-bot\.venv\Lib\site-packages\lightbulb\context\base.py", line 334, in invoke
    await self.command.invoke(self)
  File "D:\Documents\Git Stuff\hikari-test-bot\.venv\Lib\site-packages\lightbulb\commands\base.py", line 798, in invoke
    await self(context, **kwargs)
  File "D:\Documents\Git Stuff\hikari-test-bot\.venv\Lib\site-packages\lightbulb\commands\base.py", line 712, in __call__
    return await self.callback(context, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "D:\Documents\Git Stuff\hikari-test-bot\main.py", line 107, in show_level
    return await show_help(ctx)
           ^^^^^^^^^^^^^^^^^^^^
  File "D:\Documents\Git Stuff\hikari-test-bot\.venv\Lib\site-packages\lightbulb\commands\base.py", line 385, in __call__
    await self.callback(context)
  File "D:\Documents\Git Stuff\hikari-test-bot\main.py", line 88, in show_help
    await bot.rest.edit_guild(ctx.guild_id, icon="server_icon.png")
  File "D:\Documents\Git Stuff\hikari\hikari\impl\rest.py", line 2570, in edit_guild
    await asyncio.gather(*tasks)
  File "D:\Python\3.11.2\Lib\asyncio\events.py", line 80, in _run
    self._context.run(self._callback, *self._args)
  File "D:\Documents\Git Stuff\hikari\hikari\impl\rest.py", line 2549, in <lambda>
    task.add_done_callback(lambda future: body.put("icon", future.result()))
                                                           ^^^^^^^^^^^^^^^
  File "D:\Documents\Git Stuff\hikari\hikari\files.py", line 355, in data_uri
    return to_data_uri(await self.read(), self.mimetype)
                       ^^^^^^^^^^^^^^^^^
  File "D:\Documents\Git Stuff\hikari\hikari\files.py", line 360, in read
    async for chunk in self:
  File "D:\Documents\Git Stuff\hikari\hikari\files.py", line 818, in __aiter__
    chunk = await loop.run_in_executor(self._executor, self._pointer.read, _MAGIC)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "D:\Python\3.11.2\Lib\concurrent\futures\thread.py", line 58, in run
    result = self.fn(*self.args, **self.kwargs)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
ValueError: read of closed file
```

### System info

```shell
hikari (2.0.0.dev119) [2d5d5fcf]
located at D:\Documents\Git Stuff\hikari\hikari                                         
CPython 3.11.2 MSC v.1934 64 bit (AMD64)                                                
Windows nullstation 10 10.0.19044 AMD64 AMD64 Family 23 Model 8 Stepping 2, AuthenticAMD
```


### Further info

_No response_

### Checklist

- [X] I have made sure to remove ANY sensitive information (bot token, passwords, credentials, personal details, etc.).
- [X] I have searched the issue tracker and have made sure it's not a duplicate. If it is a follow up of another issue, I have specified it.

Pull request:
Use a stack to keep streams open until usage in `RESTClient.edit_guild`
### 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
Closes #1613


Patch:
diff --git a/changes/1627.bugfix.md b/changes/1627.bugfix.md
new file mode 100644
index 00000000..6520ff8b
--- /dev/null
+++ b/changes/1627.bugfix.md
@@ -0,0 +1,1 @@
+Fix a bug in `RESTClient.edit_guild` which load to closed stream errors
diff --git a/hikari/impl/rest.py b/hikari/impl/rest.py
index 0df1fee5..77030bdc 100644
--- a/hikari/impl/rest.py
+++ b/hikari/impl/rest.py
@@ -2537,36 +2537,41 @@ async def edit_guild(
         body.put_snowflake("rules_channel_id", rules_channel)
         body.put_snowflake("public_updates_channel_id", public_updates_channel)
 
+        stack = contextlib.AsyncExitStack()
         tasks: typing.List[asyncio.Task[str]] = []
 
-        if icon is None:
-            body.put("icon", None)
-        elif icon is not undefined.UNDEFINED:
-            icon_resource = files.ensure_resource(icon)
-            async with icon_resource.stream(executor=self._executor) as stream:
+        async with stack:
+            if icon is None:
+                body.put("icon", None)
+            elif icon is not undefined.UNDEFINED:
+                icon_resource = files.ensure_resource(icon)
+                stream = await stack.enter_async_context(icon_resource.stream(executor=self._executor))
+
                 task = asyncio.create_task(stream.data_uri())
                 task.add_done_callback(lambda future: body.put("icon", future.result()))
                 tasks.append(task)
 
-        if splash is None:
-            body.put("splash", None)
-        elif splash is not undefined.UNDEFINED:
-            splash_resource = files.ensure_resource(splash)
-            async with splash_resource.stream(executor=self._executor) as stream:
+            if splash is None:
+                body.put("splash", None)
+            elif splash is not undefined.UNDEFINED:
+                splash_resource = files.ensure_resource(splash)
+                stream = await stack.enter_async_context(splash_resource.stream(executor=self._executor))
+
                 task = asyncio.create_task(stream.data_uri())
                 task.add_done_callback(lambda future: body.put("splash", future.result()))
                 tasks.append(task)
 
-        if banner is None:
-            body.put("banner", None)
-        elif banner is not undefined.UNDEFINED:
-            banner_resource = files.ensure_resource(banner)
-            async with banner_resource.stream(executor=self._executor) as stream:
+            if banner is None:
+                body.put("banner", None)
+            elif banner is not undefined.UNDEFINED:
+                banner_resource = files.ensure_resource(banner)
+                stream = await stack.enter_async_context(banner_resource.stream(executor=self._executor))
+
                 task = asyncio.create_task(stream.data_uri())
                 task.add_done_callback(lambda future: body.put("banner", future.result()))
                 tasks.append(task)
 
-        await asyncio.gather(*tasks)
+            await asyncio.gather(*tasks)
 
         response = await self._request(route, json=body, reason=reason)
         assert isinstance(response, dict)


Test patch:
diff --git a/tests/hikari/impl/test_rest.py b/tests/hikari/impl/test_rest.py
index 17a6b305..972251b6 100644
--- a/tests/hikari/impl/test_rest.py
+++ b/tests/hikari/impl/test_rest.py
@@ -430,16 +430,21 @@ def rest_client(rest_client_class, mock_cache):
 def file_resource():
     class Stream:
         def __init__(self, data):
+            self.open = False
             self.data = data
 
         async def data_uri(self):
+            if not self.open:
+                raise RuntimeError("Tried to read off a closed stream")
+
             return self.data
 
         async def __aenter__(self):
+            self.open = True
             return self
 
         async def __aexit__(self, exc_type, exc, exc_tb) -> None:
-            pass
+            self.open = False
 
     class FileResource(files.Resource):
         filename = None