From 59acf8af38a72e57b26d739adb5d5e7f350e8f2c Mon Sep 17 00:00:00 2001 From: Cooper Lees Date: Mon, 22 Aug 2022 20:39:48 -0700 Subject: [PATCH] Add passing 3.11 CI by exempting blackd tests (#3234) - Had to exempt blackd tests for now due to aiohttp - Skip by using `sys.version_info` tuple - aiohttp does not compile in 3.11 yet - refer to #3230 - Add a deadsnakes ubuntu workflow to run 3.11-dev to ensure we don't regress - Have it also format ourselves Test: - `tox -e 311` Co-authored-by: Cooper Ry Lees Co-authored-by: Richard Si <63936253+ichard26@users.noreply.github.com> --- .github/workflows/test-311.yml | 57 +++++ CHANGES.md | 2 + setup.py | 1 + tests/test_blackd.py | 379 +++++++++++++++++---------------- tox.ini | 27 ++- 5 files changed, 279 insertions(+), 187 deletions(-) create mode 100644 .github/workflows/test-311.yml diff --git a/.github/workflows/test-311.yml b/.github/workflows/test-311.yml new file mode 100644 index 0000000..e23a67e --- /dev/null +++ b/.github/workflows/test-311.yml @@ -0,0 +1,57 @@ +name: Partially test 3.11 dev + +on: + push: + paths-ignore: + - "docs/**" + - "*.md" + + pull_request: + paths-ignore: + - "docs/**" + - "*.md" + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: true + +jobs: + main: + # We want to run on external PRs, but not on our own internal PRs as they'll be run + # by the push to the branch. Without this if check, checks are duplicated since + # internal PRs match both the push and pull_request events. + if: + github.event_name == 'push' || github.event.pull_request.head.repo.full_name != + github.repository + + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + python-version: ["3.11.0-rc - 3.11"] + os: [ubuntu-latest, macOS-latest, windows-latest] + + steps: + - uses: actions/checkout@v3 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + + - name: Install tox + run: | + python -m pip install --upgrade pip + python -m pip install --upgrade tox + + - name: Run tests via tox + run: | + python -m tox -e 311 + + - name: Format ourselves + run: | + python -m pip install . + python -m black --check src/ diff --git a/CHANGES.md b/CHANGES.md index fb7a272..cae2326 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -69,6 +69,8 @@ +- Python 3.11 is now supported, except for `blackd` (#3234) + ### Parser diff --git a/setup.py b/setup.py index bc0cc32..2cf4555 100644 --- a/setup.py +++ b/setup.py @@ -127,6 +127,7 @@ setup( "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3 :: Only", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Software Development :: Quality Assurance", diff --git a/tests/test_blackd.py b/tests/test_blackd.py index 1d12113..8e73906 100644 --- a/tests/test_blackd.py +++ b/tests/test_blackd.py @@ -1,4 +1,5 @@ import re +import sys from typing import Any from unittest.mock import patch @@ -7,195 +8,201 @@ from click.testing import CliRunner from tests.util import DETERMINISTIC_HEADER, read_data -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 - -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: Any, *args: Any, **kwargs: Any) -> Any: - return func - - -@pytest.mark.blackd -class BlackDTestCase(AioHTTPTestCase): - 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: +LESS_THAN_311 = sys.version_info < (3, 11) + +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 + + 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: Any, *args: Any, **kwargs: Any) -> Any: + return func + + @pytest.mark.blackd + class BlackDTestCase(AioHTTPTestCase): + 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"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=b"what", headers={blackd.PYTHON_VARIANT_HEADER: header_value} + "/", data=source, headers={blackd.DIFF_HEADER: "true"} ) - 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: + 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=code, headers={blackd.PYTHON_VARIANT_HEADER: header_value} + "/", data=b'print("hello")\n', headers={blackd.LINE_LENGTH_HEADER: "7"} ) - self.assertEqual( - response.status, expected_status, msg=await response.text() + 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) - 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")) + @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 51ff487..5f3874c 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = {,ci-}py{36,37,38,39,310,py3},fuzz,run_self +envlist = {,ci-}py{36,37,38,39,310,311,py3},fuzz,run_self [testenv] setenv = PYTHONPATH = {toxinidir}/src @@ -50,6 +50,31 @@ commands = --cov --cov-append {posargs} coverage report +[testenv:{,ci-}311] +setenv = PYTHONPATH = {toxinidir}/src +skip_install = True +recreate = True +deps = + -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 . + coverage erase + pytest tests \ + --run-optional no_jupyter \ + !ci: --numprocesses auto \ + ci: --numprocesses 1 \ + --cov {posargs} + pip install -e .[jupyter] + pytest tests --run-optional jupyter \ + -m jupyter \ + !ci: --numprocesses auto \ + ci: --numprocesses 1 \ + --cov --cov-append {posargs} + coverage report + [testenv:fuzz] skip_install = True deps = -- 2.39.5