| commit eda9eddfd1b797c23fd07751db331d37e9d347e7 |
| Author: Sviatoslav Sydorenko <wk@sydorenko.org.ua> |
| Date: Wed Feb 24 12:25:25 2021 +0100 |
| |
| Merge branch 'bugfixes/release-resources-pytest' |
| |
| This change makes sure to release most of the hanging resources |
| in the lib and tests. They were detected by pytest v6.2+. |
| |
| PR #5494 |
| |
| (cherry picked from commit 8c82ba11b9e38851d75476d261a1442402cc7592) |
| (cherry picked from commit fe5e684a8f81f3a8db69a8c063641b7d1da94f61) |
| |
| diff --git a/CHANGES/5494.bugfix b/CHANGES/5494.bugfix |
| new file mode 100644 |
| index 000000000..449b6bdf3 |
| --- /dev/null |
| +++ b/CHANGES/5494.bugfix |
| @@ -0,0 +1,4 @@ |
| +Fixed the multipart POST requests processing to always release file |
| +descriptors for the ``tempfile.Temporaryfile``-created |
| +``_io.BufferedRandom`` instances of files sent within multipart request |
| +bodies via HTTP POST requests. |
| diff --git a/CHANGES/5494.misc b/CHANGES/5494.misc |
| new file mode 100644 |
| index 000000000..3d83a77a0 |
| --- /dev/null |
| +++ b/CHANGES/5494.misc |
| @@ -0,0 +1,3 @@ |
| +Made sure to always close most of file descriptors and release other |
| +resouces in tests. Started ignoring ``ResourceWarning``s in pytest for |
| +warnings that are hard to track. |
| diff --git a/aiohttp/web_request.py b/aiohttp/web_request.py |
| index f11e7be44..9f9092059 100644 |
| --- a/aiohttp/web_request.py |
| +++ b/aiohttp/web_request.py |
| @@ -661,6 +661,7 @@ class BaseRequest(MutableMapping[str, Any], HeadersMixin): |
| tmp.write(chunk) |
| size += len(chunk) |
| if 0 < max_size < size: |
| + tmp.close() |
| raise HTTPRequestEntityTooLarge( |
| max_size=max_size, actual_size=size |
| ) |
| diff --git a/requirements/test.txt b/requirements/test.txt |
| index 3085dd588..0a9e8ffcf 100644 |
| --- a/requirements/test.txt |
| +++ b/requirements/test.txt |
| @@ -5,7 +5,7 @@ cryptography==3.2.1; platform_machine!="i686" and python_version<"3.9" # no 32-b |
| freezegun==1.0.0 |
| mypy==0.790; implementation_name=="cpython" |
| mypy-extensions==0.4.3; implementation_name=="cpython" |
| -pytest==6.1.2 |
| +pytest==6.2.2 |
| pytest-cov==2.10.1 |
| pytest-mock==3.3.1 |
| re-assert==1.1.0 |
| diff --git a/setup.cfg b/setup.cfg |
| index df8fbc315..63e7282cd 100644 |
| --- a/setup.cfg |
| +++ b/setup.cfg |
| @@ -39,6 +39,15 @@ addopts = --cov=aiohttp -v -rxXs --durations 10 |
| filterwarnings = |
| error |
| ignore:module 'ssl' has no attribute 'OP_NO_COMPRESSION'. The Python interpreter is compiled against OpenSSL < 1.0.0. Ref. https.//docs.python.org/3/library/ssl.html#ssl.OP_NO_COMPRESSION:UserWarning |
| + ignore:Exception ignored in. <function _SSLProtocolTransport.__del__ at.:pytest.PytestUnraisableExceptionWarning:_pytest.unraisableexception |
| + ignore:Exception ignored in. <coroutine object BaseConnector.close at 0x.:pytest.PytestUnraisableExceptionWarning:_pytest.unraisableexception |
| + ignore:Exception ignored in. <coroutine object ClientSession._request at 0x.:pytest.PytestUnraisableExceptionWarning:_pytest.unraisableexception |
| + ignore:Exception ignored in. <function ClientSession.__del__ at 0x.:pytest.PytestUnraisableExceptionWarning:_pytest.unraisableexception |
| + ignore:Exception ignored in. <_io.FileIO .closed.>:pytest.PytestUnraisableExceptionWarning:_pytest.unraisableexception |
| + |
| + # aiohttp 3.x: |
| + ignore:Exception ignored in. <socket.socket fd=-1, family=AddressFamily.AF_UNIX, type=SocketKind.SOCK_STREAM, proto=0>:pytest.PytestUnraisableExceptionWarning:_pytest.unraisableexception |
| + ignore:Exception ignored in. <function _DeprecationWaiter.__del__ at 0x.:pytest.PytestUnraisableExceptionWarning:_pytest.unraisableexception |
| junit_suite_name = aiohttp_test_suite |
| norecursedirs = dist docs build .tox .eggs |
| minversion = 3.8.2 |
| diff --git a/tests/test_client_request.py b/tests/test_client_request.py |
| index d6500593a..6700db461 100644 |
| --- a/tests/test_client_request.py |
| +++ b/tests/test_client_request.py |
| @@ -611,6 +611,7 @@ async def test_content_type_auto_header_get(loop, conn) -> None: |
| resp = await req.send(conn) |
| assert "CONTENT-TYPE" not in req.headers |
| resp.close() |
| + await req.close() |
| |
| |
| async def test_content_type_auto_header_form(loop, conn) -> None: |
| @@ -715,6 +716,7 @@ async def test_pass_falsy_data_file(loop, tmpdir) -> None: |
| ) |
| assert req.headers.get("CONTENT-LENGTH", None) is not None |
| await req.close() |
| + testfile.close() |
| |
| |
| # Elasticsearch API requires to send request body with GET-requests |
| diff --git a/tests/test_client_response.py b/tests/test_client_response.py |
| index 55aae9708..cec7aa320 100644 |
| --- a/tests/test_client_response.py |
| +++ b/tests/test_client_response.py |
| @@ -46,6 +46,7 @@ async def test_http_processing_error(session) -> None: |
| await response.start(connection) |
| |
| assert info.value.request_info is request_info |
| + response.close() |
| |
| |
| def test_del(session) -> None: |
| diff --git a/tests/test_client_session.py b/tests/test_client_session.py |
| index 298dac9f2..9e9e28cf2 100644 |
| --- a/tests/test_client_session.py |
| +++ b/tests/test_client_session.py |
| @@ -5,6 +5,7 @@ import json |
| import sys |
| from http.cookies import SimpleCookie |
| from io import BytesIO |
| +from typing import Any |
| from unittest import mock |
| |
| import pytest |
| @@ -30,7 +31,7 @@ def connector(loop): |
| proto = mock.Mock() |
| conn._conns["a"] = [(proto, 123)] |
| yield conn |
| - conn.close() |
| + loop.run_until_complete(conn.close()) |
| |
| |
| @pytest.fixture |
| @@ -292,7 +293,7 @@ async def test_connector(create_session, loop, mocker) -> None: |
| |
| await session.close() |
| assert connector.close.called |
| - connector.close() |
| + await connector.close() |
| |
| |
| async def test_create_connector(create_session, loop, mocker) -> None: |
| @@ -327,7 +328,7 @@ def test_connector_loop(loop) -> None: |
| ) |
| |
| |
| -def test_detach(session) -> None: |
| +def test_detach(loop: Any, session: Any) -> None: |
| conn = session.connector |
| try: |
| assert not conn.closed |
| @@ -336,7 +337,7 @@ def test_detach(session) -> None: |
| assert session.closed |
| assert not conn.closed |
| finally: |
| - conn.close() |
| + loop.run_until_complete(conn.close()) |
| |
| |
| async def test_request_closed_session(session) -> None: |
| @@ -514,6 +515,7 @@ async def test_cookie_jar_usage(loop, aiohttp_client) -> None: |
| async def test_session_default_version(loop) -> None: |
| session = aiohttp.ClientSession(loop=loop) |
| assert session.version == aiohttp.HttpVersion11 |
| + await session.close() |
| |
| |
| async def test_session_loop(loop) -> None: |
| @@ -632,6 +634,8 @@ async def test_request_tracing_exception() -> None: |
| ) |
| assert not on_request_end.called |
| |
| + await session.close() |
| + |
| |
| async def test_request_tracing_interpose_headers(loop, aiohttp_client) -> None: |
| async def handler(request): |
| @@ -674,6 +678,7 @@ async def test_client_session_custom_attr(loop) -> None: |
| session = ClientSession(loop=loop) |
| with pytest.warns(DeprecationWarning): |
| session.custom = None |
| + await session.close() |
| |
| |
| async def test_client_session_timeout_args(loop) -> None: |
| @@ -698,21 +703,25 @@ async def test_client_session_timeout_args(loop) -> None: |
| async def test_client_session_timeout_default_args(loop) -> None: |
| session1 = ClientSession() |
| assert session1.timeout == client.DEFAULT_TIMEOUT |
| + await session1.close() |
| |
| |
| async def test_client_session_timeout_argument() -> None: |
| session = ClientSession(timeout=500) |
| assert session.timeout == 500 |
| + await session.close() |
| |
| |
| async def test_requote_redirect_url_default() -> None: |
| session = ClientSession() |
| assert session.requote_redirect_url |
| + await session.close() |
| |
| |
| async def test_requote_redirect_url_default_disable() -> None: |
| session = ClientSession(requote_redirect_url=False) |
| assert not session.requote_redirect_url |
| + await session.close() |
| |
| |
| async def test_requote_redirect_setter() -> None: |
| @@ -721,3 +730,4 @@ async def test_requote_redirect_setter() -> None: |
| with pytest.warns(DeprecationWarning): |
| session.requote_redirect_url = False |
| assert not session.requote_redirect_url |
| + await session.close() |
| diff --git a/tests/test_connector.py b/tests/test_connector.py |
| index 09841923e..f9034f6db 100644 |
| --- a/tests/test_connector.py |
| +++ b/tests/test_connector.py |
| @@ -657,7 +657,7 @@ async def test_tcp_connector_multiple_hosts_errors(loop) -> None: |
| |
| conn._loop.create_connection = create_connection |
| |
| - await conn.connect(req, [], ClientTimeout()) |
| + established_connection = await conn.connect(req, [], ClientTimeout()) |
| assert ips == ips_tried |
| |
| assert os_error |
| @@ -666,6 +666,8 @@ async def test_tcp_connector_multiple_hosts_errors(loop) -> None: |
| assert fingerprint_error |
| assert connected |
| |
| + established_connection.close() |
| + |
| |
| async def test_tcp_connector_resolve_host(loop) -> None: |
| conn = aiohttp.TCPConnector(loop=loop, use_dns_cache=True) |
| @@ -1595,6 +1597,8 @@ async def test_connect_with_limit_cancelled(loop) -> None: |
| await asyncio.wait_for(conn.connect(req, None, ClientTimeout()), 0.01) |
| connection.close() |
| |
| + await conn.close() |
| + |
| |
| async def test_connect_with_capacity_release_waiters(loop) -> None: |
| def check_with_exc(err): |
| @@ -2260,3 +2264,5 @@ async def test_connector_does_not_remove_needed_waiters(loop, key) -> None: |
| await_connection_and_check_waiters(), |
| allow_connection_and_add_dummy_waiter(), |
| ) |
| + |
| + await connector.close() |
| diff --git a/tests/test_proxy.py b/tests/test_proxy.py |
| index 3b1bf0c05..541783153 100644 |
| --- a/tests/test_proxy.py |
| +++ b/tests/test_proxy.py |
| @@ -72,6 +72,8 @@ class TestProxy(unittest.TestCase): |
| ssl=None, |
| ) |
| |
| + conn.close() |
| + |
| @mock.patch("aiohttp.connector.ClientRequest") |
| def test_proxy_headers(self, ClientRequestMock) -> None: |
| req = ClientRequest( |
| @@ -112,6 +114,8 @@ class TestProxy(unittest.TestCase): |
| ssl=None, |
| ) |
| |
| + conn.close() |
| + |
| def test_proxy_auth(self) -> None: |
| with self.assertRaises(ValueError) as ctx: |
| ClientRequest( |
| diff --git a/tests/test_run_app.py b/tests/test_run_app.py |
| index d2ba2262a..deeb483f7 100644 |
| --- a/tests/test_run_app.py |
| +++ b/tests/test_run_app.py |
| @@ -630,27 +630,29 @@ web.run_app(app, host=()) |
| def test_sigint() -> None: |
| skip_if_on_windows() |
| |
| - proc = subprocess.Popen( |
| - [sys.executable, "-u", "-c", _script_test_signal], stdout=subprocess.PIPE |
| - ) |
| - for line in proc.stdout: |
| - if line.startswith(b"======== Running on"): |
| - break |
| - proc.send_signal(signal.SIGINT) |
| - assert proc.wait() == 0 |
| + with subprocess.Popen( |
| + [sys.executable, "-u", "-c", _script_test_signal], |
| + stdout=subprocess.PIPE, |
| + ) as proc: |
| + for line in proc.stdout: |
| + if line.startswith(b"======== Running on"): |
| + break |
| + proc.send_signal(signal.SIGINT) |
| + assert proc.wait() == 0 |
| |
| |
| def test_sigterm() -> None: |
| skip_if_on_windows() |
| |
| - proc = subprocess.Popen( |
| - [sys.executable, "-u", "-c", _script_test_signal], stdout=subprocess.PIPE |
| - ) |
| - for line in proc.stdout: |
| - if line.startswith(b"======== Running on"): |
| - break |
| - proc.terminate() |
| - assert proc.wait() == 0 |
| + with subprocess.Popen( |
| + [sys.executable, "-u", "-c", _script_test_signal], |
| + stdout=subprocess.PIPE, |
| + ) as proc: |
| + for line in proc.stdout: |
| + if line.startswith(b"======== Running on"): |
| + break |
| + proc.terminate() |
| + assert proc.wait() == 0 |
| |
| |
| def test_startup_cleanup_signals_even_on_failure(patched_loop) -> None: |
| diff --git a/tests/test_web_functional.py b/tests/test_web_functional.py |
| index a28fcd4f5..7810401e8 100644 |
| --- a/tests/test_web_functional.py |
| +++ b/tests/test_web_functional.py |
| @@ -4,6 +4,7 @@ import json |
| import pathlib |
| import socket |
| import zlib |
| +from typing import Any |
| from unittest import mock |
| |
| import pytest |
| @@ -324,7 +325,8 @@ async def test_post_single_file(aiohttp_client) -> None: |
| |
| fname = here / "data.unknown_mime_type" |
| |
| - resp = await client.post("/", data=[fname.open("rb")]) |
| + with fname.open("rb") as fd: |
| + resp = await client.post("/", data=[fd]) |
| assert 200 == resp.status |
| |
| |
| @@ -874,13 +876,16 @@ async def test_response_with_streamer_no_params(aiohttp_client, fname) -> None: |
| assert resp.headers.get("Content-Length") == str(len(resp_data)) |
| |
| |
| -async def test_response_with_file(aiohttp_client, fname) -> None: |
| +async def test_response_with_file(aiohttp_client: Any, fname: Any) -> None: |
| + outer_file_descriptor = None |
| |
| with fname.open("rb") as f: |
| data = f.read() |
| |
| async def handler(request): |
| - return web.Response(body=fname.open("rb")) |
| + nonlocal outer_file_descriptor |
| + outer_file_descriptor = fname.open("rb") |
| + return web.Response(body=outer_file_descriptor) |
| |
| app = web.Application() |
| app.router.add_get("/", handler) |
| @@ -901,15 +906,21 @@ async def test_response_with_file(aiohttp_client, fname) -> None: |
| assert resp.headers.get("Content-Length") == str(len(resp_data)) |
| assert resp.headers.get("Content-Disposition") == expected_content_disposition |
| |
| + outer_file_descriptor.close() |
| |
| -async def test_response_with_file_ctype(aiohttp_client, fname) -> None: |
| + |
| +async def test_response_with_file_ctype(aiohttp_client: Any, fname: Any) -> None: |
| + outer_file_descriptor = None |
| |
| with fname.open("rb") as f: |
| data = f.read() |
| |
| async def handler(request): |
| + nonlocal outer_file_descriptor |
| + outer_file_descriptor = fname.open("rb") |
| + |
| return web.Response( |
| - body=fname.open("rb"), headers={"content-type": "text/binary"} |
| + body=outer_file_descriptor, headers={"content-type": "text/binary"} |
| ) |
| |
| app = web.Application() |
| @@ -927,14 +938,19 @@ async def test_response_with_file_ctype(aiohttp_client, fname) -> None: |
| assert resp.headers.get("Content-Length") == str(len(resp_data)) |
| assert resp.headers.get("Content-Disposition") == expected_content_disposition |
| |
| + outer_file_descriptor.close() |
| + |
| |
| -async def test_response_with_payload_disp(aiohttp_client, fname) -> None: |
| +async def test_response_with_payload_disp(aiohttp_client: Any, fname: Any) -> None: |
| + outer_file_descriptor = None |
| |
| with fname.open("rb") as f: |
| data = f.read() |
| |
| async def handler(request): |
| - pl = aiohttp.get_payload(fname.open("rb")) |
| + nonlocal outer_file_descriptor |
| + outer_file_descriptor = fname.open("rb") |
| + pl = aiohttp.get_payload(outer_file_descriptor) |
| pl.set_content_disposition("inline", filename="test.txt") |
| return web.Response(body=pl, headers={"content-type": "text/binary"}) |
| |
| @@ -953,6 +969,8 @@ async def test_response_with_payload_disp(aiohttp_client, fname) -> None: |
| == "inline; filename=\"test.txt\"; filename*=utf-8''test.txt" |
| ) |
| |
| + outer_file_descriptor.close() |
| + |
| |
| async def test_response_with_payload_stringio(aiohttp_client, fname) -> None: |
| async def handler(request): |
| @@ -1565,6 +1583,7 @@ async def test_post_max_client_size(aiohttp_client) -> None: |
| assert ( |
| "Maximum request body size 10 exceeded, " "actual body size 1024" in resp_text |
| ) |
| + data["file"].close() |
| |
| |
| async def test_post_max_client_size_for_file(aiohttp_client) -> None: |
| @@ -1618,11 +1637,12 @@ async def test_response_with_bodypart_named(aiohttp_client, tmpdir) -> None: |
| |
| f = tmpdir.join("foobar.txt") |
| f.write_text("test", encoding="utf8") |
| - data = {"file": open(str(f), "rb")} |
| - resp = await client.post("/", data=data) |
| + with open(str(f), "rb") as fd: |
| + data = {"file": fd} |
| + resp = await client.post("/", data=data) |
| |
| - assert 200 == resp.status |
| - body = await resp.read() |
| + assert 200 == resp.status |
| + body = await resp.read() |
| assert body == b"test" |
| |
| disp = multipart.parse_content_disposition(resp.headers["content-disposition"]) |
| @@ -1700,12 +1720,15 @@ async def test_response_context_manager(aiohttp_server) -> None: |
| app = web.Application() |
| app.router.add_route("GET", "/", handler) |
| server = await aiohttp_server(app) |
| - resp = await aiohttp.ClientSession().get(server.make_url("/")) |
| + session = aiohttp.ClientSession() |
| + resp = await session.get(server.make_url("/")) |
| async with resp: |
| assert resp.status == 200 |
| assert resp.connection is None |
| assert resp.connection is None |
| |
| + await session.close() |
| + |
| |
| async def test_response_context_manager_error(aiohttp_server) -> None: |
| async def handler(request): |
| @@ -1726,6 +1749,8 @@ async def test_response_context_manager_error(aiohttp_server) -> None: |
| |
| assert len(session._connector._conns) == 1 |
| |
| + await session.close() |
| + |
| |
| async def aiohttp_client_api_context_manager(aiohttp_server): |
| async def handler(request): |
| diff --git a/tests/test_web_urldispatcher.py b/tests/test_web_urldispatcher.py |
| index 0ba2e7c20..b45a27bd3 100644 |
| --- a/tests/test_web_urldispatcher.py |
| +++ b/tests/test_web_urldispatcher.py |
| @@ -152,6 +152,7 @@ async def test_access_to_the_file_with_spaces( |
| r = await client.get(url) |
| assert r.status == 200 |
| assert (await r.text()) == data |
| + await r.release() |
| |
| |
| async def test_access_non_existing_resource(tmp_dir_path, aiohttp_client) -> None: |