diff --git a/.github/workflows/test-311.yml b/.github/workflows/test-311.yml index e23a67e89eb..c2da2465ad5 100644 --- a/.github/workflows/test-311.yml +++ b/.github/workflows/test-311.yml @@ -1,4 +1,4 @@ -name: Partially test 3.11 dev +name: Test 3.11 without aiohttp extensions on: push: diff --git a/pyproject.toml b/pyproject.toml index 849891f8798..566462c9b36 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -111,4 +111,7 @@ filterwarnings = [ # this is mitigated by https://github.com/python/cpython/issues/79071 in python 3.8+ # this ignore can be removed when support for 3.7 is dropped. '''ignore:Bare functions are deprecated, use async ones:DeprecationWarning''', + # aiohttp is using deprecated cgi modules - Safe to remove when fixed: + # https://github.com/aio-libs/aiohttp/issues/6905 + '''ignore:'cgi' is deprecated and slated for removal in Python 3.13:DeprecationWarning''', ] diff --git a/tests/test_blackd.py b/tests/test_blackd.py index 1da4ab702d2..511bd86441d 100644 --- a/tests/test_blackd.py +++ b/tests/test_blackd.py @@ -1,5 +1,4 @@ import re -import sys from typing import TYPE_CHECKING, Any, Callable, TypeVar from unittest.mock import patch @@ -8,207 +7,205 @@ from tests.util import DETERMINISTIC_HEADER, read_data -LESS_THAN_311 = sys.version_info < (3, 11) +try: + from aiohttp import web + from aiohttp.test_utils import AioHTTPTestCase -if LESS_THAN_311: # noqa: C901 - try: - from aiohttp import web - from aiohttp.test_utils import AioHTTPTestCase - - import blackd - except ImportError as e: - raise RuntimeError("Please install Black with the 'd' extra") from e - - if TYPE_CHECKING: - F = TypeVar("F", bound=Callable[..., Any]) - - unittest_run_loop: Callable[[F], F] = lambda x: x - else: - try: - from aiohttp.test_utils import unittest_run_loop - except ImportError: - # unittest_run_loop is unnecessary and a no-op since aiohttp 3.8, and - # aiohttp 4 removed it. To maintain compatibility we can make our own - # no-op decorator. - def unittest_run_loop(func, *args, **kwargs): - return func - - @pytest.mark.blackd - class BlackDTestCase(AioHTTPTestCase): # type: ignore[misc] - def test_blackd_main(self) -> None: - with patch("blackd.web.run_app"): - result = CliRunner().invoke(blackd.main, []) - if result.exception is not None: - raise result.exception - self.assertEqual(result.exit_code, 0) - - async def get_application(self) -> web.Application: - return blackd.make_app() - - @unittest_run_loop - async def test_blackd_request_needs_formatting(self) -> None: - response = await self.client.post("/", data=b"print('hello world')") - self.assertEqual(response.status, 200) - self.assertEqual(response.charset, "utf8") - self.assertEqual(await response.read(), b'print("hello world")\n') - - @unittest_run_loop - async def test_blackd_request_no_change(self) -> None: - response = await self.client.post("/", data=b'print("hello world")\n') - self.assertEqual(response.status, 204) - self.assertEqual(await response.read(), b"") - - @unittest_run_loop - async def test_blackd_request_syntax_error(self) -> None: - response = await self.client.post("/", data=b"what even ( is") - self.assertEqual(response.status, 400) - content = await response.text() - self.assertTrue( - content.startswith("Cannot parse"), - msg=f"Expected error to start with 'Cannot parse', got {repr(content)}", - ) + import blackd +except ImportError as e: + raise RuntimeError("Please install Black with the 'd' extra") from e - @unittest_run_loop - async def test_blackd_unsupported_version(self) -> None: - response = await self.client.post( - "/", data=b"what", headers={blackd.PROTOCOL_VERSION_HEADER: "2"} - ) - self.assertEqual(response.status, 501) +if TYPE_CHECKING: + F = TypeVar("F", bound=Callable[..., Any]) - @unittest_run_loop - async def test_blackd_supported_version(self) -> None: - response = await self.client.post( - "/", data=b"what", headers={blackd.PROTOCOL_VERSION_HEADER: "1"} - ) - self.assertEqual(response.status, 200) - - @unittest_run_loop - async def test_blackd_invalid_python_variant(self) -> None: - async def check(header_value: str, expected_status: int = 400) -> None: - response = await self.client.post( - "/", - data=b"what", - headers={blackd.PYTHON_VARIANT_HEADER: header_value}, - ) - self.assertEqual(response.status, expected_status) - - await check("lol") - await check("ruby3.5") - await check("pyi3.6") - await check("py1.5") - await check("2") - await check("2.7") - await check("py2.7") - await check("2.8") - await check("py2.8") - await check("3.0") - await check("pypy3.0") - await check("jython3.4") - - @unittest_run_loop - async def test_blackd_pyi(self) -> None: - source, expected = read_data("miscellaneous", "stub.pyi") - response = await self.client.post( - "/", data=source, headers={blackd.PYTHON_VARIANT_HEADER: "pyi"} - ) - self.assertEqual(response.status, 200) - self.assertEqual(await response.text(), expected) - - @unittest_run_loop - async def test_blackd_diff(self) -> None: - diff_header = re.compile( - r"(In|Out)\t\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d" - ) - - source, _ = read_data("miscellaneous", "blackd_diff") - expected, _ = read_data("miscellaneous", "blackd_diff.diff") - - response = await self.client.post( - "/", data=source, headers={blackd.DIFF_HEADER: "true"} - ) - self.assertEqual(response.status, 200) - - actual = await response.text() - actual = diff_header.sub(DETERMINISTIC_HEADER, actual) - self.assertEqual(actual, expected) - - @unittest_run_loop - async def test_blackd_python_variant(self) -> None: - code = ( - "def f(\n" - " and_has_a_bunch_of,\n" - " very_long_arguments_too,\n" - " and_lots_of_them_as_well_lol,\n" - " **and_very_long_keyword_arguments\n" - "):\n" - " pass\n" - ) - - async def check(header_value: str, expected_status: int) -> None: - response = await self.client.post( - "/", data=code, headers={blackd.PYTHON_VARIANT_HEADER: header_value} - ) - self.assertEqual( - response.status, expected_status, msg=await response.text() - ) - - await check("3.6", 200) - await check("py3.6", 200) - await check("3.6,3.7", 200) - await check("3.6,py3.7", 200) - await check("py36,py37", 200) - await check("36", 200) - await check("3.6.4", 200) - await check("3.4", 204) - await check("py3.4", 204) - await check("py34,py36", 204) - await check("34", 204) - - @unittest_run_loop - async def test_blackd_line_length(self) -> None: - response = await self.client.post( - "/", data=b'print("hello")\n', headers={blackd.LINE_LENGTH_HEADER: "7"} - ) - self.assertEqual(response.status, 200) - - @unittest_run_loop - async def test_blackd_invalid_line_length(self) -> None: + unittest_run_loop: Callable[[F], F] = lambda x: x +else: + try: + from aiohttp.test_utils import unittest_run_loop + except ImportError: + # unittest_run_loop is unnecessary and a no-op since aiohttp 3.8, and + # aiohttp 4 removed it. To maintain compatibility we can make our own + # no-op decorator. + def unittest_run_loop(func, *args, **kwargs): + return func + + +@pytest.mark.blackd +class BlackDTestCase(AioHTTPTestCase): # type: ignore[misc] + def test_blackd_main(self) -> None: + with patch("blackd.web.run_app"): + result = CliRunner().invoke(blackd.main, []) + if result.exception is not None: + raise result.exception + self.assertEqual(result.exit_code, 0) + + async def get_application(self) -> web.Application: + return blackd.make_app() + + @unittest_run_loop + async def test_blackd_request_needs_formatting(self) -> None: + response = await self.client.post("/", data=b"print('hello world')") + self.assertEqual(response.status, 200) + self.assertEqual(response.charset, "utf8") + self.assertEqual(await response.read(), b'print("hello world")\n') + + @unittest_run_loop + async def test_blackd_request_no_change(self) -> None: + response = await self.client.post("/", data=b'print("hello world")\n') + self.assertEqual(response.status, 204) + self.assertEqual(await response.read(), b"") + + @unittest_run_loop + async def test_blackd_request_syntax_error(self) -> None: + response = await self.client.post("/", data=b"what even ( is") + self.assertEqual(response.status, 400) + content = await response.text() + self.assertTrue( + content.startswith("Cannot parse"), + msg=f"Expected error to start with 'Cannot parse', got {repr(content)}", + ) + + @unittest_run_loop + async def test_blackd_unsupported_version(self) -> None: + response = await self.client.post( + "/", data=b"what", headers={blackd.PROTOCOL_VERSION_HEADER: "2"} + ) + self.assertEqual(response.status, 501) + + @unittest_run_loop + async def test_blackd_supported_version(self) -> None: + response = await self.client.post( + "/", data=b"what", headers={blackd.PROTOCOL_VERSION_HEADER: "1"} + ) + self.assertEqual(response.status, 200) + + @unittest_run_loop + async def test_blackd_invalid_python_variant(self) -> None: + async def check(header_value: str, expected_status: int = 400) -> None: response = await self.client.post( "/", - data=b'print("hello")\n', - headers={blackd.LINE_LENGTH_HEADER: "NaN"}, + data=b"what", + headers={blackd.PYTHON_VARIANT_HEADER: header_value}, ) - self.assertEqual(response.status, 400) - - @unittest_run_loop - async def test_blackd_preview(self) -> None: + self.assertEqual(response.status, expected_status) + + await check("lol") + await check("ruby3.5") + await check("pyi3.6") + await check("py1.5") + await check("2") + await check("2.7") + await check("py2.7") + await check("2.8") + await check("py2.8") + await check("3.0") + await check("pypy3.0") + await check("jython3.4") + + @unittest_run_loop + async def test_blackd_pyi(self) -> None: + source, expected = read_data("miscellaneous", "stub.pyi") + response = await self.client.post( + "/", data=source, headers={blackd.PYTHON_VARIANT_HEADER: "pyi"} + ) + self.assertEqual(response.status, 200) + self.assertEqual(await response.text(), expected) + + @unittest_run_loop + async def test_blackd_diff(self) -> None: + diff_header = re.compile( + r"(In|Out)\t\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d" + ) + + source, _ = read_data("miscellaneous", "blackd_diff") + expected, _ = read_data("miscellaneous", "blackd_diff.diff") + + response = await self.client.post( + "/", data=source, headers={blackd.DIFF_HEADER: "true"} + ) + self.assertEqual(response.status, 200) + + actual = await response.text() + actual = diff_header.sub(DETERMINISTIC_HEADER, actual) + self.assertEqual(actual, expected) + + @unittest_run_loop + async def test_blackd_python_variant(self) -> None: + code = ( + "def f(\n" + " and_has_a_bunch_of,\n" + " very_long_arguments_too,\n" + " and_lots_of_them_as_well_lol,\n" + " **and_very_long_keyword_arguments\n" + "):\n" + " pass\n" + ) + + async def check(header_value: str, expected_status: int) -> None: response = await self.client.post( - "/", data=b'print("hello")\n', headers={blackd.PREVIEW: "true"} + "/", data=code, headers={blackd.PYTHON_VARIANT_HEADER: header_value} ) - self.assertEqual(response.status, 204) - - @unittest_run_loop - async def test_blackd_response_black_version_header(self) -> None: - response = await self.client.post("/") - self.assertIsNotNone(response.headers.get(blackd.BLACK_VERSION_HEADER)) - - @unittest_run_loop - async def test_cors_preflight(self) -> None: - response = await self.client.options( - "/", - headers={ - "Access-Control-Request-Method": "POST", - "Origin": "*", - "Access-Control-Request-Headers": "Content-Type", - }, + self.assertEqual( + response.status, expected_status, msg=await response.text() ) - self.assertEqual(response.status, 200) - self.assertIsNotNone(response.headers.get("Access-Control-Allow-Origin")) - self.assertIsNotNone(response.headers.get("Access-Control-Allow-Headers")) - self.assertIsNotNone(response.headers.get("Access-Control-Allow-Methods")) - - @unittest_run_loop - async def test_cors_headers_present(self) -> None: - response = await self.client.post("/", headers={"Origin": "*"}) - self.assertIsNotNone(response.headers.get("Access-Control-Allow-Origin")) - self.assertIsNotNone(response.headers.get("Access-Control-Expose-Headers")) + + await check("3.6", 200) + await check("py3.6", 200) + await check("3.6,3.7", 200) + await check("3.6,py3.7", 200) + await check("py36,py37", 200) + await check("36", 200) + await check("3.6.4", 200) + await check("3.4", 204) + await check("py3.4", 204) + await check("py34,py36", 204) + await check("34", 204) + + @unittest_run_loop + async def test_blackd_line_length(self) -> None: + response = await self.client.post( + "/", data=b'print("hello")\n', headers={blackd.LINE_LENGTH_HEADER: "7"} + ) + self.assertEqual(response.status, 200) + + @unittest_run_loop + async def test_blackd_invalid_line_length(self) -> None: + response = await self.client.post( + "/", + data=b'print("hello")\n', + headers={blackd.LINE_LENGTH_HEADER: "NaN"}, + ) + self.assertEqual(response.status, 400) + + @unittest_run_loop + async def test_blackd_preview(self) -> None: + response = await self.client.post( + "/", data=b'print("hello")\n', headers={blackd.PREVIEW: "true"} + ) + self.assertEqual(response.status, 204) + + @unittest_run_loop + async def test_blackd_response_black_version_header(self) -> None: + response = await self.client.post("/") + self.assertIsNotNone(response.headers.get(blackd.BLACK_VERSION_HEADER)) + + @unittest_run_loop + async def test_cors_preflight(self) -> None: + response = await self.client.options( + "/", + headers={ + "Access-Control-Request-Method": "POST", + "Origin": "*", + "Access-Control-Request-Headers": "Content-Type", + }, + ) + self.assertEqual(response.status, 200) + self.assertIsNotNone(response.headers.get("Access-Control-Allow-Origin")) + self.assertIsNotNone(response.headers.get("Access-Control-Allow-Headers")) + self.assertIsNotNone(response.headers.get("Access-Control-Allow-Methods")) + + @unittest_run_loop + async def test_cors_headers_present(self) -> None: + response = await self.client.post("/", headers={"Origin": "*"}) + self.assertIsNotNone(response.headers.get("Access-Control-Allow-Origin")) + self.assertIsNotNone(response.headers.get("Access-Control-Expose-Headers")) diff --git a/tox.ini b/tox.ini index 5f3874c23b4..098f06c9828 100644 --- a/tox.ini +++ b/tox.ini @@ -51,16 +51,20 @@ commands = coverage report [testenv:{,ci-}311] -setenv = PYTHONPATH = {toxinidir}/src +setenv = + PYTHONPATH = {toxinidir}/src + AIOHTTP_NO_EXTENSIONS = 1 skip_install = True recreate = True deps = +; We currently need > aiohttp 3.8.1 that is on PyPI for 3.11 + git+https://github.com/aio-libs/aiohttp -r{toxinidir}/test_requirements.txt ; a separate worker is required in ci due to https://foss.heptapod.net/pypy/pypy/-/issues/3317 ; this seems to cause tox to wait forever ; remove this when pypy releases the bugfix commands = - pip install -e . + pip install -e .[d] coverage erase pytest tests \ --run-optional no_jupyter \