Repository: aiogram/aiogram
Pull request #1238 (2023-07-31 17:04:32)

Reworked InputFile sending
# Description

Reworked InputFile sending

github-actions[bot]: # :x: Changelog is required!

You need to add a brief description of the changes to the `CHANGES` directory.

For example, you can run `towncrier create <issue>.<type>` to create a file in the change directory and then write a description on that file.

Read more at [Towncrier docs](https://towncrier.readthedocs.io/en/latest/quickstart.html#creating-news-fragments)
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/aiogram/aiogram/pull/1238?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram) Report
> Merging [#1238](https://app.codecov.io/gh/aiogram/aiogram/pull/1238?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram) (ef1a806) into [dev-3.x](https://app.codecov.io/gh/aiogram/aiogram/commit/faadc8046032d64350c03eebe6f88a36cc4a2d23?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram) (faadc80) will **not change** coverage.
> Report is 3 commits behind head on dev-3.x.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://app.codecov.io/gh/aiogram/aiogram/pull/1238/graphs/tree.svg?width=650&height=150&src=pr&token=cFqEr3p9MX&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram)](https://app.codecov.io/gh/aiogram/aiogram/pull/1238?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram)

```diff
@@            Coverage Diff            @@
##           dev-3.x     #1238   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files          363       363           
  Lines         9127      9125    -2     
=========================================
- Hits          9127      9125    -2     
```

| Flag | Coverage Δ | |
|---|---|---|
| unittests | `100.00% <100.00%> (ø)` | |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Files Changed](https://app.codecov.io/gh/aiogram/aiogram/pull/1238?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram) | Coverage Δ | |
|---|---|---|
| [aiogram/client/session/aiohttp.py](https://app.codecov.io/gh/aiogram/aiogram/pull/1238?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram#diff-YWlvZ3JhbS9jbGllbnQvc2Vzc2lvbi9haW9odHRwLnB5) | `100.00% <100.00%> (ø)` | |
| [aiogram/types/input\_file.py](https://app.codecov.io/gh/aiogram/aiogram/pull/1238?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram#diff-YWlvZ3JhbS90eXBlcy9pbnB1dF9maWxlLnB5) | `100.00% <100.00%> (ø)` | |
| [aiogram/webhook/aiohttp\_server.py](https://app.codecov.io/gh/aiogram/aiogram/pull/1238?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aiogram#diff-YWlvZ3JhbS93ZWJob29rL2Fpb2h0dHBfc2VydmVyLnB5) | `100.00% <100.00%> (ø)` | |


Patch:
diff --git a/CHANGES/1238.misc.rst b/CHANGES/1238.misc.rst
new file mode 100644
index 00000000..4f62cae1
--- /dev/null
+++ b/CHANGES/1238.misc.rst
@@ -0,0 +1,3 @@
+Reworked InputFile reading, removed :code:`__aiter__` method, added `bot: Bot` argument to
+the :code:`.read(...)` method, so, from now URLInputFile can be used without specifying
+bot instance.
diff --git a/aiogram/__meta__.py b/aiogram/__meta__.py
index f2a9528b..ca7a61cc 100644
--- a/aiogram/__meta__.py
+++ b/aiogram/__meta__.py
@@ -1,2 +1,2 @@
-__version__ = "3.0.0b9"
+__version__ = "3.0.0rc1"
 __api_version__ = "6.7"
diff --git a/aiogram/client/session/aiohttp.py b/aiogram/client/session/aiohttp.py
index 79e2fa4f..19a8edbd 100644
--- a/aiogram/client/session/aiohttp.py
+++ b/aiogram/client/session/aiohttp.py
@@ -6,6 +6,7 @@ from typing import (
     TYPE_CHECKING,
     Any,
     AsyncGenerator,
+    AsyncIterator,
     Dict,
     Iterable,
     List,
@@ -147,7 +148,11 @@ class AiohttpSession(BaseSession):
                 continue
             form.add_field(key, value)
         for key, value in files.items():
-            form.add_field(key, value, filename=value.filename or key)
+            form.add_field(
+                key,
+                value.read(bot),
+                filename=value.filename or key,
+            )
         return form
 
     async def make_request(
diff --git a/aiogram/types/input_file.py b/aiogram/types/input_file.py
index ed0a2433..7ad8bee3 100644
--- a/aiogram/types/input_file.py
+++ b/aiogram/types/input_file.py
@@ -41,13 +41,9 @@ class InputFile(ABC):
         self.chunk_size = chunk_size
 
     @abstractmethod
-    async def read(self, chunk_size: int) -> AsyncGenerator[bytes, None]:  # pragma: no cover
+    async def read(self, bot: "Bot") -> AsyncGenerator[bytes, None]:  # pragma: no cover
         yield b""
 
-    async def __aiter__(self) -> AsyncIterator[bytes]:
-        async for chunk in self.read(self.chunk_size):
-            yield chunk
-
 
 class BufferedInputFile(InputFile):
     def __init__(self, file: bytes, filename: str, chunk_size: int = DEFAULT_CHUNK_SIZE):
@@ -84,9 +80,9 @@ class BufferedInputFile(InputFile):
             data = f.read()
         return cls(data, filename=filename, chunk_size=chunk_size)
 
-    async def read(self, chunk_size: int) -> AsyncGenerator[bytes, None]:
+    async def read(self, bot: "Bot") -> AsyncGenerator[bytes, None]:
         buffer = io.BytesIO(self.data)
-        while chunk := buffer.read(chunk_size):
+        while chunk := buffer.read(self.chunk_size):
             yield chunk
 
 
@@ -111,9 +107,9 @@ class FSInputFile(InputFile):
 
         self.path = path
 
-    async def read(self, chunk_size: int) -> AsyncGenerator[bytes, None]:
+    async def read(self, bot: "Bot") -> AsyncGenerator[bytes, None]:
         async with aiofiles.open(self.path, "rb") as f:
-            while chunk := await f.read(chunk_size):
+            while chunk := await f.read(self.chunk_size):
                 yield chunk
 
 
@@ -121,11 +117,11 @@ class URLInputFile(InputFile):
     def __init__(
         self,
         url: str,
-        bot: "Bot",
         headers: Optional[Dict[str, Any]] = None,
         filename: Optional[str] = None,
         chunk_size: int = DEFAULT_CHUNK_SIZE,
         timeout: int = 30,
+        bot: Optional["Bot"] = None,
     ):
         """
         Represents object for streaming files from internet
@@ -136,7 +132,7 @@ class URLInputFile(InputFile):
         :param chunk_size: Uploading chunk size
         :param timeout: Timeout for downloading
         :param bot: Bot instance to use HTTP session from.
-                    If not specified, will be used current bot from context.
+                    If not specified, will be used current bot
         """
         super().__init__(filename=filename, chunk_size=chunk_size)
         if headers is None:
@@ -147,8 +143,9 @@ class URLInputFile(InputFile):
         self.timeout = timeout
         self.bot = bot
 
-    async def read(self, chunk_size: int) -> AsyncGenerator[bytes, None]:
-        stream = self.bot.session.stream_content(
+    async def read(self, bot: "Bot") -> AsyncGenerator[bytes, None]:
+        bot = self.bot or bot
+        stream = bot.session.stream_content(
             url=self.url,
             headers=self.headers,
             timeout=self.timeout,
diff --git a/aiogram/webhook/aiohttp_server.py b/aiogram/webhook/aiohttp_server.py
index f623484f..af1c3c56 100644
--- a/aiogram/webhook/aiohttp_server.py
+++ b/aiogram/webhook/aiohttp_server.py
@@ -170,7 +170,7 @@ class BaseRequestHandler(ABC):
             payload.set_content_disposition("form-data", name=key)
 
         for key, value in files.items():
-            payload = writer.append(value)
+            payload = writer.append(value.read(bot))
             payload.set_content_disposition(
                 "form-data",
                 name=key,
diff --git a/pyproject.toml b/pyproject.toml
index d4471685..a50e9449 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -24,7 +24,7 @@ keywords = [
 ]
 classifiers = [
     "License :: OSI Approved :: MIT License",
-    "Development Status :: 4 - Beta",
+    "Development Status :: 5 - Production/Stable",
     "Environment :: Console",
     "Framework :: AsyncIO",
     "Typing :: Typed",


Test patch:
diff --git a/tests/test_api/test_client/test_session/test_aiohttp_session.py b/tests/test_api/test_client/test_session/test_aiohttp_session.py
index fd7cabfd..d79eb875 100644
--- a/tests/test_api/test_client/test_session/test_aiohttp_session.py
+++ b/tests/test_api/test_client/test_session/test_aiohttp_session.py
@@ -1,5 +1,5 @@
 import asyncio
-from typing import Any, AsyncContextManager, AsyncGenerator, Dict, List
+from typing import Any, AsyncContextManager, AsyncGenerator, AsyncIterable, Dict, List
 from unittest.mock import AsyncMock, patch
 
 import aiohttp_socks
@@ -156,7 +156,7 @@ class TestAiohttpSession:
         assert fields[1][2].startswith("attach://")
         assert fields[2][0]["name"] == fields[1][2][9:]
         assert fields[2][0]["filename"] == "file.txt"
-        assert isinstance(fields[2][2], BareInputFile)
+        assert isinstance(fields[2][2], AsyncIterable)
 
     async def test_make_request(self, bot: MockedBot, aresponses: ResponsesMockServer):
         aresponses.add(
diff --git a/tests/test_api/test_types/test_chat_join_request.py b/tests/test_api/test_types/test_chat_join_request.py
index a36dd935..ec442cb1 100644
--- a/tests/test_api/test_types/test_chat_join_request.py
+++ b/tests/test_api/test_types/test_chat_join_request.py
@@ -24,11 +24,7 @@ from aiogram.methods import (
     SendVideoNote,
     SendVoice,
 )
-from aiogram.types import (
-    Chat,
-    ChatJoinRequest,
-    User,
-)
+from aiogram.types import Chat, ChatJoinRequest, User
 
 
 class TestChatJoinRequest:
diff --git a/tests/test_api/test_types/test_input_file.py b/tests/test_api/test_types/test_input_file.py
index 81e80ad5..28daa92c 100644
--- a/tests/test_api/test_types/test_input_file.py
+++ b/tests/test_api/test_types/test_input_file.py
@@ -12,19 +12,18 @@ class TestInputFile:
         file = FSInputFile(__file__)
 
         assert isinstance(file, InputFile)
-        assert isinstance(file, AsyncIterable)
         assert file.filename is not None
         assert file.filename.startswith("test_")
         assert file.filename.endswith(".py")
         assert file.chunk_size > 0
 
-    async def test_fs_input_file_readable(self):
+    async def test_fs_input_file_readable(self, bot: MockedBot):
         file = FSInputFile(__file__, chunk_size=1)
 
         assert file.chunk_size == 1
 
         size = 0
-        async for chunk in file:
+        async for chunk in file.read(bot):
             chunk_size = len(chunk)
             assert chunk_size == 1
             size += chunk_size
@@ -34,15 +33,14 @@ class TestInputFile:
         file = BufferedInputFile(b"\f" * 10, filename="file.bin")
 
         assert isinstance(file, InputFile)
-        assert isinstance(file, AsyncIterable)
         assert file.filename == "file.bin"
         assert isinstance(file.data, bytes)
 
-    async def test_buffered_input_file_readable(self):
+    async def test_buffered_input_file_readable(self, bot: MockedBot):
         file = BufferedInputFile(b"\f" * 10, filename="file.bin", chunk_size=1)
 
         size = 0
-        async for chunk in file:
+        async for chunk in file.read(bot):
             chunk_size = len(chunk)
             assert chunk_size == 1
             size += chunk_size
@@ -52,32 +50,31 @@ class TestInputFile:
         file = BufferedInputFile.from_file(__file__, chunk_size=10)
 
         assert isinstance(file, InputFile)
-        assert isinstance(file, AsyncIterable)
         assert file.filename is not None
         assert file.filename.startswith("test_")
         assert file.filename.endswith(".py")
         assert isinstance(file.data, bytes)
         assert file.chunk_size == 10
 
-    async def test_buffered_input_file_from_file_readable(self):
+    async def test_buffered_input_file_from_file_readable(self, bot: MockedBot):
         file = BufferedInputFile.from_file(__file__, chunk_size=1)
 
         size = 0
-        async for chunk in file:
+        async for chunk in file.read(bot):
             chunk_size = len(chunk)
             assert chunk_size == 1
             size += chunk_size
         assert size > 0
 
-    async def test_uri_input_file(self, aresponses: ResponsesMockServer):
+    async def test_url_input_file(self, aresponses: ResponsesMockServer):
         aresponses.add(
             aresponses.ANY, aresponses.ANY, "get", aresponses.Response(status=200, body=b"\f" * 10)
         )
         bot = Bot(token="42:TEST")
-        file = URLInputFile("https://test.org/", bot, chunk_size=1)
+        file = URLInputFile("https://test.org/", chunk_size=1)
 
         size = 0
-        async for chunk in file:
+        async for chunk in file.read(bot):
             assert chunk == b"\f"
             chunk_size = len(chunk)
             assert chunk_size == 1