-
Notifications
You must be signed in to change notification settings - Fork 5.8k
Description
Steps to Reproduce
- Install
python-telegram-bot
,pytest
andpytest-asyncio
. - Prepare a photo (e.g.,
photo.png
). - Use the following minimal
pytest
test case, which enablesResourceWarning
as an error.
from pathlib import Path
import pytest
from telegram.ext import ApplicationBuilder
@pytest.mark.filterwarnings("error::ResourceWarning")
@pytest.mark.asyncio
async def test_send_photo_with_path_generates_warning(tmp_path):
async with (
ApplicationBuilder()
.token("******:************")
.build()
.bot as bot
):
await bot.send_photo(chat_id='*********', photo=Path(r"C:\Users\vivodi\Downloads\logo.png"))
- Run the test with
pytest
.
Expected behaviour
The method should internally manage the file handle it creates. It should open the file from the provided path, read its contents, and then immediately close the file handle.
No ResourceWarning
should be emitted. The pytest
test above should pass without errors.
Actual behaviour
When passing a pathlib.Path
object to bot.send_photo()
or bot.send_document()
, the underlying file is opened but is not subsequently closed. This results in a ResourceWarning
when the file object is garbage collected.
This becomes a critical issue in environments that treat warnings as errors, such as in a CI/CD pipeline using pytest
with the -W error
flag, causing tests to fail.
The file handle created from the pathlib.Path
object is not closed. This generates a ResourceWarning
.
The pytest
test fails with the following traceback, pointing to the unclosed file resource.
E ResourceWarning: unclosed file <_io.BufferedReader name='...\\photo.png'>
.../venv/lib/site-packages/telegram/_utils/files.py:148: ResourceWarning
=========================== short test summary info ===========================
FAILED test_file_handling.py::test_send_photo_with_path_generates_warning
The issue originates in telegram._utils.files.parse_file_input
, where path.open()
is called but the returned file object is never closed.
python-telegram-bot/src/telegram/_utils/files.py
Lines 143 to 148 in 4654d19
if isinstance(file_input, (str, Path)): | |
if is_local_file(file_input): | |
path = Path(file_input) | |
if local_mode: | |
return path.absolute().as_uri() | |
return InputFile(path.open(mode="rb"), filename=filename, attach=attach) |
Operating System
Windows 11
Version of Python, python-telegram-bot & dependencies
22.3
Relevant log output
============================= test session starts =============================
platform win32 -- Python 3.14.0rc1, pytest-8.4.1, pluggy-1.6.0 -- C:\Users\vivodi\Projects\Flexget\.venv\Scripts\python.exe
cachedir: .pytest_cache
rootdir: C:\Users\vivodi\Projects\Flexget
configfile: pyproject.toml
plugins: anyio-4.10.0, asyncio-1.1.0, cov-6.2.1, xdist-3.8.0
asyncio: mode=Mode.STRICT, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function
created: 12/12 workers
12 workers [1 item]
scheduling tests via LoadGroupScheduling
tests/test_t.py::test_send_photo_with_path_generates_warning
================================== FAILURES ===================================
_________________ test_send_photo_with_path_generates_warning _________________
[gw0] win32 -- Python 3.14.0 C:\Users\vivodi\Projects\Flexget\.venv\Scripts\python.exe
file_input = WindowsPath('C:/Users/vivodi/Downloads/logo.png')
tg_type = <class 'telegram._files.photosize.PhotoSize'>, filename = None
attach = False, local_mode = False
def parse_file_input( # pylint: disable=too-many-return-statements
file_input: Union[FileInput, "TelegramObject"],
tg_type: Optional[type["TelegramObject"]] = None,
filename: Optional[str] = None,
attach: bool = False,
local_mode: bool = False,
) -> Union[str, "InputFile", Any]:
"""
Parses input for sending files:
* For string input, if the input is an absolute path of a local file:
* if ``local_mode`` is ``True``, adds the ``file://`` prefix. If the input is a relative
path of a local file, computes the absolute path and adds the ``file://`` prefix.
* if ``local_mode`` is ``False``, loads the file as binary data and builds an
:class:`InputFile` from that
Returns the input unchanged, otherwise.
* :class:`pathlib.Path` objects are treated the same way as strings.
* For IO and bytes input, returns an :class:`telegram.InputFile`.
* If :attr:`tg_type` is specified and the input is of that type, returns the ``file_id``
attribute.
Args:
file_input (:obj:`str` | :obj:`bytes` | :term:`file object` | :class:`~telegram.InputFile`\
| Telegram media object): The input to parse.
tg_type (:obj:`type`, optional): The Telegram media type the input can be. E.g.
:class:`telegram.Animation`.
filename (:obj:`str`, optional): The filename. Only relevant in case an
:class:`telegram.InputFile` is returned.
attach (:obj:`bool`, optional): Pass :obj:`True` if the parameter this file belongs to in
the request to Telegram should point to the multipart data via an ``attach://`` URI.
Defaults to `False`. Only relevant if an :class:`telegram.InputFile` is returned.
local_mode (:obj:`bool`, optional): Pass :obj:`True` if the bot is running an api server
in ``--local`` mode.
Returns:
:obj:`str` | :class:`telegram.InputFile` | :obj:`object`: The parsed input or the untouched
:attr:`file_input`, in case it's no valid file input.
"""
# Importing on file-level yields cyclic Import Errors
from telegram import InputFile # pylint: disable=import-outside-toplevel # noqa: PLC0415
if isinstance(file_input, str) and file_input.startswith("file://"):
if not local_mode:
raise ValueError("Specified file input is a file URI, but local mode is not enabled.")
return file_input
if isinstance(file_input, (str, Path)):
if is_local_file(file_input):
path = Path(file_input)
if local_mode:
return path.absolute().as_uri()
> return InputFile(path.open(mode="rb"), filename=filename, attach=attach)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E ResourceWarning: unclosed file <_io.BufferedReader name='C:\\Users\\vivodi\\Downloads\\logo.png'>
C:\Users\vivodi\Projects\Flexget\.venv\Lib\site-packages\telegram\_utils\files.py:148: ResourceWarning
The above exception was the direct cause of the following exception:
cls = <class '_pytest.runner.CallInfo'>
func = <function call_and_report.<locals>.<lambda> at 0x0000014645816C40>
when = 'call'
reraise = (<class '_pytest.outcomes.Exit'>, <class 'KeyboardInterrupt'>)
@classmethod
def from_call(
cls,
func: Callable[[], TResult],
when: Literal["collect", "setup", "call", "teardown"],
reraise: type[BaseException] | tuple[type[BaseException], ...] | None = None,
) -> CallInfo[TResult]:
"""Call func, wrapping the result in a CallInfo.
:param func:
The function to call. Called without arguments.
:type func: Callable[[], _pytest.runner.TResult]
:param when:
The phase in which the function is called.
:param reraise:
Exception or exceptions that shall propagate if raised by the
function, instead of being wrapped in the CallInfo.
"""
excinfo = None
instant = timing.Instant()
try:
> result: TResult | None = func()
^^^^^^
C:\Users\vivodi\Projects\Flexget\.venv\Lib\site-packages\_pytest\runner.py:344:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
C:\Users\vivodi\Projects\Flexget\.venv\Lib\site-packages\_pytest\runner.py:246: in <lambda>
lambda: runtest_hook(item=item, **kwds), when=when, reraise=reraise
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
C:\Users\vivodi\Projects\Flexget\.venv\Lib\site-packages\pluggy\_hooks.py:512: in __call__
return self._hookexec(self.name, self._hookimpls.copy(), kwargs, firstresult)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
C:\Users\vivodi\Projects\Flexget\.venv\Lib\site-packages\pluggy\_manager.py:120: in _hookexec
return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
C:\Users\vivodi\Projects\Flexget\.venv\Lib\site-packages\_pytest\logging.py:850: in pytest_runtest_call
yield
C:\Users\vivodi\Projects\Flexget\.venv\Lib\site-packages\_pytest\capture.py:900: in pytest_runtest_call
return (yield)
^^^^^
C:\Users\vivodi\Projects\Flexget\.venv\Lib\site-packages\_pytest\skipping.py:263: in pytest_runtest_call
return (yield)
^^^^^
C:\Users\vivodi\Projects\Flexget\.venv\Lib\site-packages\_pytest\unraisableexception.py:158: in pytest_runtest_call
collect_unraisable(item.config)
C:\Users\vivodi\Projects\Flexget\.venv\Lib\site-packages\_pytest\unraisableexception.py:79: in collect_unraisable
raise errors[0]
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
config = <_pytest.config.Config object at 0x000001463C1C2120>
def collect_unraisable(config: Config) -> None:
pop_unraisable = config.stash[unraisable_exceptions].pop
errors: list[pytest.PytestUnraisableExceptionWarning | RuntimeError] = []
meta = None
hook_error = None
try:
while True:
try:
meta = pop_unraisable()
except IndexError:
break
if isinstance(meta, BaseException):
hook_error = RuntimeError("Failed to process unraisable exception")
hook_error.__cause__ = meta
errors.append(hook_error)
continue
msg = meta.msg
try:
> warnings.warn(pytest.PytestUnraisableExceptionWarning(msg))
E pytest.PytestUnraisableExceptionWarning: Exception ignored while finalizing file <_io.FileIO name='C:\\Users\\vivodi\\Downloads\\logo.png' mode='rb' closefd=True>: None
C:\Users\vivodi\Projects\Flexget\.venv\Lib\site-packages\_pytest\unraisableexception.py:67: PytestUnraisableExceptionWarning
----------------------------- Captured log setup ------------------------------
DEBUG asyncio:proactor_events.py:631 Using proactor: IocpProactor
------------------------------ Captured log call ------------------------------
DEBUG telegram.ext.ExtBot:_bot.py:333 Set Bot API URL: https://api.telegram.org/bot************:************************
DEBUG telegram.ext.ExtBot:_bot.py:334 Set Bot API File URL: https://api.telegram.org/file/bot************:************************
DEBUG telegram.ext.ExtBot:_bot.py:726 Calling Bot API endpoint `getMe` with parameters `{}`
DEBUG httpcore.connection:_trace.py:87 connect_tcp.started host='api.telegram.org' port=443 local_address=None timeout=5.0 socket_options=None
DEBUG httpcore.connection:_trace.py:87 connect_tcp.complete return_value=<httpcore._backends.anyio.AnyIOStream object at 0x0000014645C1E900>
DEBUG httpcore.connection:_trace.py:87 start_tls.started ssl_context=<ssl.SSLContext object at 0x0000014645A13110> server_hostname='api.telegram.org' timeout=5.0
DEBUG httpcore.connection:_trace.py:87 start_tls.complete return_value=<httpcore._backends.anyio.AnyIOStream object at 0x0000014645C225D0>
DEBUG httpcore.http11:_trace.py:87 send_request_headers.started request=<Request [b'POST']>
DEBUG httpcore.http11:_trace.py:87 send_request_headers.complete
DEBUG httpcore.http11:_trace.py:87 send_request_body.started request=<Request [b'POST']>
DEBUG httpcore.http11:_trace.py:87 send_request_body.complete
DEBUG httpcore.http11:_trace.py:87 receive_response_headers.started request=<Request [b'POST']>
DEBUG httpcore.http11:_trace.py:87 receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'OK', [(b'Server', b'nginx/1.18.0'), (b'Date', b'Wed, 13 Aug 2025 12:33:36 GMT'), (b'Content-Type', b'application/json'), (b'Content-Length', b'251'), (b'Connection', b'keep-alive'), (b'Strict-Transport-Security', b'max-age=31536000; includeSubDomains; preload'), (b'Access-Control-Allow-Origin', b'*'), (b'Access-Control-Allow-Methods', b'GET, POST, OPTIONS'), (b'Access-Control-Expose-Headers', b'Content-Length,Content-Type,Date,Server,Connection')])
INFO httpx:_client.py:1740 HTTP Request: POST https://api.telegram.org/bot************:************************/getMe "HTTP/1.1 200 OK"
DEBUG httpcore.http11:_trace.py:87 receive_response_body.started request=<Request [b'POST']>
DEBUG httpcore.http11:_trace.py:87 receive_response_body.complete
DEBUG httpcore.http11:_trace.py:87 response_closed.started
DEBUG httpcore.http11:_trace.py:87 response_closed.complete
DEBUG telegram.ext.ExtBot:_bot.py:735 Call to Bot API endpoint `getMe` finished with return value `{'id': 5187592617, 'is_bot': True, 'first_name': 'oneofptbots', 'username': 'oneofptbot', 'can_join_groups': True, 'can_read_all_group_messages': False, 'supports_inline_queries': False, 'can_connect_to_business': False, 'has_main_web_app': False}`
DEBUG telegram.ext.ExtBot:_bot.py:726 Calling Bot API endpoint `sendPhoto` with parameters `{'chat_id': '12345678', 'photo': <telegram._files.inputfile.InputFile object at 0x0000014645C13B00>}`
DEBUG httpcore.http11:_trace.py:87 send_request_headers.started request=<Request [b'POST']>
DEBUG httpcore.http11:_trace.py:87 send_request_headers.complete
DEBUG httpcore.http11:_trace.py:87 send_request_body.started request=<Request [b'POST']>
DEBUG httpcore.http11:_trace.py:87 send_request_body.complete
DEBUG httpcore.http11:_trace.py:87 receive_response_headers.started request=<Request [b'POST']>
DEBUG httpcore.http11:_trace.py:87 receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'OK', [(b'Server', b'nginx/1.18.0'), (b'Date', b'Wed, 13 Aug 2025 12:33:37 GMT'), (b'Content-Type', b'application/json'), (b'Content-Length', b'778'), (b'Connection', b'keep-alive'), (b'Strict-Transport-Security', b'max-age=31536000; includeSubDomains; preload'), (b'Access-Control-Allow-Origin', b'*'), (b'Access-Control-Allow-Methods', b'GET, POST, OPTIONS'), (b'Access-Control-Expose-Headers', b'Content-Length,Content-Type,Date,Server,Connection')])
INFO httpx:_client.py:1740 HTTP Request: POST https://api.telegram.org/bot************:************************/sendPhoto "HTTP/1.1 200 OK"
DEBUG httpcore.http11:_trace.py:87 receive_response_body.started request=<Request [b'POST']>
DEBUG httpcore.http11:_trace.py:87 receive_response_body.complete
DEBUG httpcore.http11:_trace.py:87 response_closed.started
DEBUG httpcore.http11:_trace.py:87 response_closed.complete
DEBUG telegram.ext.ExtBot:_bot.py:735 Call to Bot API endpoint `sendPhoto` finished with return value `{'message_id': 3095, 'from': {'id': 5187592617, 'is_bot': True, 'first_name': 'oneofptbots', 'username': 'oneofptbot'}, 'chat': {'id': 12345678, 'first_name': 'o', 'last_name': '0', 'username': 'chehxlpp', 'type': 'private'}, 'date': 1755088417, 'photo': [{'file_id': 'AgACAgUAAxkDAAIMFGichK1MJVfAlG8CFpMCpXAcaJF2AAIPyjEbvBroVBfWvmfZKAfVAQADAgADcwADNgQ', 'file_unique_id': 'AQADD8oxG7wa6FR4', 'file_size': 780, 'width': 90, 'height': 18}, {'file_id': 'AgACAgUAAxkDAAIMFGichK1MJVfAlG8CFpMCpXAcaJF2AAIPyjEbvBroVBfWvmfZKAfVAQADAgADbQADNgQ', 'file_unique_id': 'AQADD8oxG7wa6FRy', 'file_size': 5446, 'width': 320, 'height': 65}, {'file_id': 'AgACAgUAAxkDAAIMFGichK1MJVfAlG8CFpMCpXAcaJF2AAIPyjEbvBroVBfWvmfZKAfVAQADAgADeAADNgQ', 'file_unique_id': 'AQADD8oxG7wa6FR9', 'file_size': 5767, 'width': 331, 'height': 67}]}`
DEBUG httpcore.connection:_trace.py:87 close.started
DEBUG httpcore.connection:_trace.py:87 close.complete
=========================== short test summary info ===========================
FAILED tests/test_t.py::test_send_photo_with_path_generates_warning - pytest.PytestUnraisableExceptionWarning: Exception ignored while finalizing file <_io.FileIO name='C:\\Users\\vivodi\\Downloads\\logo.png' mode='rb' closefd=True>: None
============================= 1 failed in 13.26s ==============================
[gw0] [100%] FAILED tests/test_t.py::test_send_photo_with_path_generates_warning
tests\test_t.py:7 (test_send_photo_with_path_generates_warning)
file_input = WindowsPath('C:/Users/vivodi/Downloads/logo.png')
tg_type = <class 'telegram._files.photosize.PhotoSize'>, filename = None
attach = False, local_mode = False
def parse_file_input( # pylint: disable=too-many-return-statements
file_input: Union[FileInput, "TelegramObject"],
tg_type: Optional[type["TelegramObject"]] = None,
filename: Optional[str] = None,
attach: bool = False,
local_mode: bool = False,
) -> Union[str, "InputFile", Any]:
"""
Parses input for sending files:
* For string input, if the input is an absolute path of a local file:
* if ``local_mode`` is ``True``, adds the ``file://`` prefix. If the input is a relative
path of a local file, computes the absolute path and adds the ``file://`` prefix.
* if ``local_mode`` is ``False``, loads the file as binary data and builds an
:class:`InputFile` from that
Returns the input unchanged, otherwise.
* :class:`pathlib.Path` objects are treated the same way as strings.
* For IO and bytes input, returns an :class:`telegram.InputFile`.
* If :attr:`tg_type` is specified and the input is of that type, returns the ``file_id``
attribute.
Args:
file_input (:obj:`str` | :obj:`bytes` | :term:`file object` | :class:`~telegram.InputFile`\
| Telegram media object): The input to parse.
tg_type (:obj:`type`, optional): The Telegram media type the input can be. E.g.
:class:`telegram.Animation`.
filename (:obj:`str`, optional): The filename. Only relevant in case an
:class:`telegram.InputFile` is returned.
attach (:obj:`bool`, optional): Pass :obj:`True` if the parameter this file belongs to in
the request to Telegram should point to the multipart data via an ``attach://`` URI.
Defaults to `False`. Only relevant if an :class:`telegram.InputFile` is returned.
local_mode (:obj:`bool`, optional): Pass :obj:`True` if the bot is running an api server
in ``--local`` mode.
Returns:
:obj:`str` | :class:`telegram.InputFile` | :obj:`object`: The parsed input or the untouched
:attr:`file_input`, in case it's no valid file input.
"""
# Importing on file-level yields cyclic Import Errors
from telegram import InputFile # pylint: disable=import-outside-toplevel # noqa: PLC0415
if isinstance(file_input, str) and file_input.startswith("file://"):
if not local_mode:
raise ValueError("Specified file input is a file URI, but local mode is not enabled.")
return file_input
if isinstance(file_input, (str, Path)):
if is_local_file(file_input):
path = Path(file_input)
if local_mode:
return path.absolute().as_uri()
> return InputFile(path.open(mode="rb"), filename=filename, attach=attach)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E ResourceWarning: unclosed file <_io.BufferedReader name='C:\\Users\\vivodi\\Downloads\\logo.png'>
C:\Users\vivodi\Projects\Flexget\.venv\Lib\site-packages\telegram\_utils\files.py:148: ResourceWarning
The above exception was the direct cause of the following exception:
cls = <class '_pytest.runner.CallInfo'>
func = <function call_and_report.<locals>.<lambda> at 0x0000014645816C40>
when = 'call'
reraise = (<class '_pytest.outcomes.Exit'>, <class 'KeyboardInterrupt'>)
@classmethod
def from_call(
cls,
func: Callable[[], TResult],
when: Literal["collect", "setup", "call", "teardown"],
reraise: type[BaseException] | tuple[type[BaseException], ...] | None = None,
) -> CallInfo[TResult]:
"""Call func, wrapping the result in a CallInfo.
:param func:
The function to call. Called without arguments.
:type func: Callable[[], _pytest.runner.TResult]
:param when:
The phase in which the function is called.
:param reraise:
Exception or exceptions that shall propagate if raised by the
function, instead of being wrapped in the CallInfo.
"""
excinfo = None
instant = timing.Instant()
try:
> result: TResult | None = func()
^^^^^^
C:\Users\vivodi\Projects\Flexget\.venv\Lib\site-packages\_pytest\runner.py:344:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
C:\Users\vivodi\Projects\Flexget\.venv\Lib\site-packages\_pytest\runner.py:246: in <lambda>
lambda: runtest_hook(item=item, **kwds), when=when, reraise=reraise
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
C:\Users\vivodi\Projects\Flexget\.venv\Lib\site-packages\pluggy\_hooks.py:512: in __call__
return self._hookexec(self.name, self._hookimpls.copy(), kwargs, firstresult)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
C:\Users\vivodi\Projects\Flexget\.venv\Lib\site-packages\pluggy\_manager.py:120: in _hookexec
return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
C:\Users\vivodi\Projects\Flexget\.venv\Lib\site-packages\_pytest\logging.py:850: in pytest_runtest_call
yield
C:\Users\vivodi\Projects\Flexget\.venv\Lib\site-packages\_pytest\capture.py:900: in pytest_runtest_call
return (yield)
^^^^^
C:\Users\vivodi\Projects\Flexget\.venv\Lib\site-packages\_pytest\skipping.py:263: in pytest_runtest_call
return (yield)
^^^^^
C:\Users\vivodi\Projects\Flexget\.venv\Lib\site-packages\_pytest\unraisableexception.py:158: in pytest_runtest_call
collect_unraisable(item.config)
C:\Users\vivodi\Projects\Flexget\.venv\Lib\site-packages\_pytest\unraisableexception.py:79: in collect_unraisable
raise errors[0]
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
config = <_pytest.config.Config object at 0x000001463C1C2120>
def collect_unraisable(config: Config) -> None:
pop_unraisable = config.stash[unraisable_exceptions].pop
errors: list[pytest.PytestUnraisableExceptionWarning | RuntimeError] = []
meta = None
hook_error = None
try:
while True:
try:
meta = pop_unraisable()
except IndexError:
break
if isinstance(meta, BaseException):
hook_error = RuntimeError("Failed to process unraisable exception")
hook_error.__cause__ = meta
errors.append(hook_error)
continue
msg = meta.msg
try:
> warnings.warn(pytest.PytestUnraisableExceptionWarning(msg))
E pytest.PytestUnraisableExceptionWarning: Exception ignored while finalizing file <_io.FileIO name='C:\\Users\\vivodi\\Downloads\\logo.png' mode='rb' closefd=True>: None
C:\Users\vivodi\Projects\Flexget\.venv\Lib\site-packages\_pytest\unraisableexception.py:67: PytestUnraisableExceptionWarning
Additional Context
Workaround
The current workaround is to manually manage the file handle in the user's code, which prevents the library from opening the file itself.
# This works correctly and does not generate a warning
image_path = Path("photo.png")
with open(image_path, "rb") as f:
await bot.send_photo(chat_id=chat_id, photo=f)
While this workaround is effective, the library should ideally handle this gracefully when given a path.