encode / starlette

The little ASGI framework that shines. 🌟
https://www.starlette.io/
BSD 3-Clause "New" or "Revised" License
10.31k stars 949 forks source link

Python 3.13.0b2: `test_gzip_ignored_for_responses_with_encoding_set[trio]` fails with a `ValueError: I/O operation on closed file` #2615

Closed befeleme closed 3 months ago

befeleme commented 5 months ago

Since this is a different failure than in #2614, I decided to go for another issue.

___________ test_gzip_ignored_for_responses_with_encoding_set[trio] ____________

cls = <class '_pytest.runner.CallInfo'>
func = <function call_runtest_hook.<locals>.<lambda> at 0x7f0fd00a2840>
when = 'call'
reraise = (<class '_pytest.outcomes.Exit'>, <class 'KeyboardInterrupt'>)

    @classmethod
    def from_call(
        cls,
        func: "Callable[[], TResult]",
        when: "Literal['collect', 'setup', 'call', 'teardown']",
        reraise: Optional[
            Union[Type[BaseException], Tuple[Type[BaseException], ...]]
        ] = None,
    ) -> "CallInfo[TResult]":
        """Call func, wrapping the result in a CallInfo.

        :param func:
            The function to call. Called without arguments.
        :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
        start = timing.time()
        precise_start = timing.perf_counter()
        try:
>           result: Optional[TResult] = func()

/usr/lib/python3.13/site-packages/_pytest/runner.py:341: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
/usr/lib/python3.13/site-packages/_pytest/runner.py:262: in <lambda>
    lambda: ihook(item=item, **kwds), when=when, reraise=reraise
/usr/lib/python3.13/site-packages/pluggy/_hooks.py:493: in __call__
    return self._hookexec(self.name, self._hookimpls, kwargs, firstresult)
/usr/lib/python3.13/site-packages/pluggy/_manager.py:115: in _hookexec
    return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
/usr/lib/python3.13/site-packages/_pytest/unraisableexception.py:88: in pytest_runtest_call
    yield from unraisable_exception_runtest_hook()
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

    def unraisable_exception_runtest_hook() -> Generator[None, None, None]:
        with catch_unraisable_exception() as cm:
            yield
            if cm.unraisable:
                if cm.unraisable.err_msg is not None:
                    err_msg = cm.unraisable.err_msg
                else:
                    err_msg = "Exception ignored in"
                msg = f"{err_msg}: {cm.unraisable.object!r}\n\n"
                msg += "".join(
                    traceback.format_exception(
                        cm.unraisable.exc_type,
                        cm.unraisable.exc_value,
                        cm.unraisable.exc_traceback,
                    )
                )
>               warnings.warn(pytest.PytestUnraisableExceptionWarning(msg))
E               pytest.PytestUnraisableExceptionWarning: Exception ignored in: <gzip on 0x7f0fd0276380>
E               
E               Traceback (most recent call last):
E                 File "/usr/lib64/python3.13/gzip.py", line 359, in close
E                   fileobj.write(self.compress.flush())
E                   ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^
E               ValueError: I/O operation on closed file.

[!IMPORTANT]

  • We're using Polar.sh so you can upvote and help fund this issue.
  • We receive the funding once the issue is completed & confirmed by you.
  • Thank you in advance for helping prioritize & fund our backlog.

Fund with Polar

musicinmybrain commented 5 months ago

If I do

$ gh repo clone encode/starlette
$ cd starlette
$ python3.13 -m venv _e
$ . _e/bin/activate
$ python --version
Python 3.13.0b1
$ pip install -e .[full]
$ pip install -r requirements.txt
$ pip install --upgrade trio
$ python -m pytest

then I see four test failures: test_debug_html[asyncio] and test_debug_html[trio] as reported in https://github.com/encode/starlette/issues/2614, test_gzip_ignored_for_responses_with_encoding_set[trio] as reported here, and then another apparently-similar one:

=================================================================================================== FAILURES ===================================================================================================
_____________________________________________________________________________________ test_https_redirect_middleware[trio] _____________________________________________________________________________________

cls = <class '_pytest.runner.CallInfo'>, func = <function call_runtest_hook.<locals>.<lambda> at 0x7f630459ef20>, when = 'call', reraise = (<class '_pytest.outcomes.Exit'>, <class 'KeyboardInterrupt'>)

    @classmethod
    def from_call(
        cls,
        func: Callable[[], TResult],
        when: Literal["collect", "setup", "call", "teardown"],
        reraise: Optional[
            Union[Type[BaseException], Tuple[Type[BaseException], ...]]
        ] = None,
    ) -> "CallInfo[TResult]":
        """Call func, wrapping the result in a CallInfo.

        :param func:
            The function to call. Called without arguments.
        :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
        start = timing.time()
        precise_start = timing.perf_counter()
        try:
>           result: Optional[TResult] = func()

_e/lib64/python3.13/site-packages/_pytest/runner.py:342:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
_e/lib64/python3.13/site-packages/_pytest/runner.py:263: in <lambda>
    lambda: ihook(item=item, **kwds), when=when, reraise=reraise   
_e/lib64/python3.13/site-packages/pluggy/_hooks.py:513: in __call__
    return self._hookexec(self.name, self._hookimpls.copy(), kwargs, firstresult)
_e/lib64/python3.13/site-packages/pluggy/_manager.py:120: in _hookexec
    return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
_e/lib64/python3.13/site-packages/_pytest/threadexception.py:87: in pytest_runtest_call
    yield from thread_exception_runtest_hook()
_e/lib64/python3.13/site-packages/_pytest/threadexception.py:63: in thread_exception_runtest_hook
    yield
_e/lib64/python3.13/site-packages/_pytest/unraisableexception.py:90: in pytest_runtest_call
    yield from unraisable_exception_runtest_hook()
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

    def unraisable_exception_runtest_hook() -> Generator[None, None, None]:
        with catch_unraisable_exception() as cm:
            try:
                yield
            finally:
                if cm.unraisable:
                    if cm.unraisable.err_msg is not None:
                        err_msg = cm.unraisable.err_msg
                    else:
                        err_msg = "Exception ignored in"
                    msg = f"{err_msg}: {cm.unraisable.object!r}\n\n"
                    msg += "".join(
                        traceback.format_exception(
                            cm.unraisable.exc_type,
                            cm.unraisable.exc_value,
                            cm.unraisable.exc_traceback,
                        )
                    )
>                   warnings.warn(pytest.PytestUnraisableExceptionWarning(msg))
E                   pytest.PytestUnraisableExceptionWarning: Exception ignored in: <gzip on 0x7f630458f2b0>
E                   
E                   Traceback (most recent call last):
E                     File "/usr/lib64/python3.13/gzip.py", line 359, in close
E                       fileobj.write(self.compress.flush())
E                       ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^
E                   ValueError: I/O operation on closed file.

_e/lib64/python3.13/site-packages/_pytest/unraisableexception.py:80: PytestUnraisableExceptionWarning
musicinmybrain commented 5 months ago

If I skip all four failures above,

$ python -m pytest -k 'not test_gzip_ignored_for_responses_with_encoding_set[asyncio] and not test_debug_html[asyncio] and not test_debug_html[trio] and not test_https_redirect_middleware[trio]'

a fifth one arises:

____________________________________________________________________________________________ test_session[asyncio] _____________________________________________________________________________________________

cls = <class '_pytest.runner.CallInfo'>, func = <function call_runtest_hook.<locals>.<lambda> at 0x7ff47e8823e0>, when = 'call', reraise = (<class '_pytest.outcomes.Exit'>, <class 'KeyboardInterrupt'>)

    @classmethod
    def from_call(
        cls,
        func: Callable[[], TResult],
        when: Literal["collect", "setup", "call", "teardown"],
        reraise: Optional[
            Union[Type[BaseException], Tuple[Type[BaseException], ...]]
        ] = None,
    ) -> "CallInfo[TResult]":
        """Call func, wrapping the result in a CallInfo.

        :param func:
            The function to call. Called without arguments.
        :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
        start = timing.time()
        precise_start = timing.perf_counter()
        try:
>           result: Optional[TResult] = func()

_e/lib64/python3.13/site-packages/_pytest/runner.py:342:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
_e/lib64/python3.13/site-packages/_pytest/runner.py:263: in <lambda>
    lambda: ihook(item=item, **kwds), when=when, reraise=reraise
_e/lib64/python3.13/site-packages/pluggy/_hooks.py:513: in __call__
    return self._hookexec(self.name, self._hookimpls.copy(), kwargs, firstresult)
_e/lib64/python3.13/site-packages/pluggy/_manager.py:120: in _hookexec
    return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
_e/lib64/python3.13/site-packages/_pytest/threadexception.py:87: in pytest_runtest_call
    yield from thread_exception_runtest_hook()
_e/lib64/python3.13/site-packages/_pytest/threadexception.py:63: in thread_exception_runtest_hook
    yield
_e/lib64/python3.13/site-packages/_pytest/unraisableexception.py:90: in pytest_runtest_call
    yield from unraisable_exception_runtest_hook()
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

    def unraisable_exception_runtest_hook() -> Generator[None, None, None]:
        with catch_unraisable_exception() as cm:
            try:
                yield
            finally:
                if cm.unraisable:
                    if cm.unraisable.err_msg is not None:
                        err_msg = cm.unraisable.err_msg
                    else:
                        err_msg = "Exception ignored in"
                    msg = f"{err_msg}: {cm.unraisable.object!r}\n\n"
                    msg += "".join(
                        traceback.format_exception(
                            cm.unraisable.exc_type,
                            cm.unraisable.exc_value,
                            cm.unraisable.exc_traceback,
                        )
                    )
>                   warnings.warn(pytest.PytestUnraisableExceptionWarning(msg))
E                   pytest.PytestUnraisableExceptionWarning: Exception ignored in: <gzip on 0x7ff47e81bd60>
E                   
E                   Traceback (most recent call last):
E                     File "/usr/lib64/python3.13/gzip.py", line 359, in close
E                       fileobj.write(self.compress.flush())
E                       ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^
E                   ValueError: I/O operation on closed file.

_e/lib64/python3.13/site-packages/_pytest/unraisableexception.py:80: PytestUnraisableExceptionWarning

and then if I add that to the skipped tests, test_session_expires[asyncio] fails similarly, and if I skip that, test_secure_session[asyncio] fails; if I skip that, then test_session_cookie_subpath[asyncio]; and that’s where I stopped following the chain for now.

musicinmybrain commented 5 months ago

I see this error coming from Starlette in many of the FastAPI tests in Fedora Rawhide/F41 as well.

musicinmybrain commented 4 months ago

I confirm this still happens with Python 3.13.0b4.

Kludex commented 3 months ago

Can someone report this on CPython?

This fails on 3.13...

import gzip
import io

file = io.BytesIO()
gzip_file = gzip.GzipFile(mode="wb", fileobj=file, compresslevel=9)

gzip_file.write(b"Hello, world!")

This also fails, btw:

import gzip
import io

with io.BytesIO() as buffer:
    gzip_file = gzip.GzipFile(mode="wb", fileobj=buffer, compresslevel=9)

    gzip_file.write(b"Hello, world!")
Kludex commented 3 months ago

Closing this, please follow https://github.com/python/cpython/issues/122727.