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 <me@wcooperlees.com>
Co-authored-by: Richard Si <63936253+ichard26@users.noreply.github.com>
This commit is contained in:
Cooper Lees 2022-08-22 20:39:48 -07:00 committed by GitHub
parent 6e0ad52e7a
commit 59acf8af38
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 263 additions and 171 deletions

57
.github/workflows/test-311.yml vendored Normal file
View File

@ -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/

View File

@ -69,6 +69,8 @@
<!-- Changes to how Black is packaged, such as dependency requirements --> <!-- Changes to how Black is packaged, such as dependency requirements -->
- Python 3.11 is now supported, except for `blackd` (#3234)
### Parser ### Parser
<!-- Changes to the parser or to version autodetection --> <!-- Changes to the parser or to version autodetection -->

View File

@ -127,6 +127,7 @@ def find_python_files(base: Path) -> List[Path]:
"Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3 :: Only",
"Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Software Development :: Libraries :: Python Modules",
"Topic :: Software Development :: Quality Assurance", "Topic :: Software Development :: Quality Assurance",

View File

@ -1,4 +1,5 @@
import re import re
import sys
from typing import Any from typing import Any
from unittest.mock import patch from unittest.mock import patch
@ -7,195 +8,201 @@
from tests.util import DETERMINISTIC_HEADER, read_data from tests.util import DETERMINISTIC_HEADER, read_data
try: LESS_THAN_311 = sys.version_info < (3, 11)
from aiohttp import web
from aiohttp.test_utils import AioHTTPTestCase
import blackd if LESS_THAN_311: # noqa: C901
except ImportError as e: try:
raise RuntimeError("Please install Black with the 'd' extra") from e from aiohttp import web
from aiohttp.test_utils import AioHTTPTestCase
try: import blackd
from aiohttp.test_utils import unittest_run_loop except ImportError as e:
except ImportError: raise RuntimeError("Please install Black with the 'd' extra") from e
# 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
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 @pytest.mark.blackd
class BlackDTestCase(AioHTTPTestCase): class BlackDTestCase(AioHTTPTestCase):
def test_blackd_main(self) -> None: def test_blackd_main(self) -> None:
with patch("blackd.web.run_app"): with patch("blackd.web.run_app"):
result = CliRunner().invoke(blackd.main, []) result = CliRunner().invoke(blackd.main, [])
if result.exception is not None: if result.exception is not None:
raise result.exception raise result.exception
self.assertEqual(result.exit_code, 0) self.assertEqual(result.exit_code, 0)
async def get_application(self) -> web.Application: async def get_application(self) -> web.Application:
return blackd.make_app() return blackd.make_app()
@unittest_run_loop @unittest_run_loop
async def test_blackd_request_needs_formatting(self) -> None: async def test_blackd_request_needs_formatting(self) -> None:
response = await self.client.post("/", data=b"print('hello world')") response = await self.client.post("/", data=b"print('hello world')")
self.assertEqual(response.status, 200) self.assertEqual(response.status, 200)
self.assertEqual(response.charset, "utf8") self.assertEqual(response.charset, "utf8")
self.assertEqual(await response.read(), b'print("hello world")\n') self.assertEqual(await response.read(), b'print("hello world")\n')
@unittest_run_loop @unittest_run_loop
async def test_blackd_request_no_change(self) -> None: async def test_blackd_request_no_change(self) -> None:
response = await self.client.post("/", data=b'print("hello world")\n') response = await self.client.post("/", data=b'print("hello world")\n')
self.assertEqual(response.status, 204) self.assertEqual(response.status, 204)
self.assertEqual(await response.read(), b"") self.assertEqual(await response.read(), b"")
@unittest_run_loop @unittest_run_loop
async def test_blackd_request_syntax_error(self) -> None: async def test_blackd_request_syntax_error(self) -> None:
response = await self.client.post("/", data=b"what even ( is") response = await self.client.post("/", data=b"what even ( is")
self.assertEqual(response.status, 400) self.assertEqual(response.status, 400)
content = await response.text() content = await response.text()
self.assertTrue( self.assertTrue(
content.startswith("Cannot parse"), content.startswith("Cannot parse"),
msg=f"Expected error to start with 'Cannot parse', got {repr(content)}", msg=f"Expected error to start with 'Cannot parse', got {repr(content)}",
) )
@unittest_run_loop @unittest_run_loop
async def test_blackd_unsupported_version(self) -> None: 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( response = await self.client.post(
"/", data=b"what", headers={blackd.PYTHON_VARIANT_HEADER: header_value} "/", data=b"what", headers={blackd.PROTOCOL_VERSION_HEADER: "2"}
) )
self.assertEqual(response.status, expected_status) self.assertEqual(response.status, 501)
await check("lol") @unittest_run_loop
await check("ruby3.5") async def test_blackd_supported_version(self) -> None:
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( response = await self.client.post(
"/", data=code, headers={blackd.PYTHON_VARIANT_HEADER: header_value} "/", data=b"what", headers={blackd.PROTOCOL_VERSION_HEADER: "1"}
) )
self.assertEqual( self.assertEqual(response.status, 200)
response.status, expected_status, msg=await response.text()
@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"
) )
await check("3.6", 200) source, _ = read_data("miscellaneous", "blackd_diff")
await check("py3.6", 200) expected, _ = read_data("miscellaneous", "blackd_diff.diff")
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 response = await self.client.post(
async def test_blackd_line_length(self) -> None: "/", data=source, headers={blackd.DIFF_HEADER: "true"}
response = await self.client.post( )
"/", data=b'print("hello")\n', headers={blackd.LINE_LENGTH_HEADER: "7"} self.assertEqual(response.status, 200)
)
self.assertEqual(response.status, 200)
@unittest_run_loop actual = await response.text()
async def test_blackd_invalid_line_length(self) -> None: actual = diff_header.sub(DETERMINISTIC_HEADER, actual)
response = await self.client.post( self.assertEqual(actual, expected)
"/", data=b'print("hello")\n', headers={blackd.LINE_LENGTH_HEADER: "NaN"}
)
self.assertEqual(response.status, 400)
@unittest_run_loop @unittest_run_loop
async def test_blackd_preview(self) -> None: async def test_blackd_python_variant(self) -> None:
response = await self.client.post( code = (
"/", data=b'print("hello")\n', headers={blackd.PREVIEW: "true"} "def f(\n"
) " and_has_a_bunch_of,\n"
self.assertEqual(response.status, 204) " very_long_arguments_too,\n"
" and_lots_of_them_as_well_lol,\n"
" **and_very_long_keyword_arguments\n"
"):\n"
" pass\n"
)
@unittest_run_loop async def check(header_value: str, expected_status: int) -> None:
async def test_blackd_response_black_version_header(self) -> None: response = await self.client.post(
response = await self.client.post("/") "/", data=code, headers={blackd.PYTHON_VARIANT_HEADER: header_value}
self.assertIsNotNone(response.headers.get(blackd.BLACK_VERSION_HEADER)) )
self.assertEqual(
response.status, expected_status, msg=await response.text()
)
@unittest_run_loop await check("3.6", 200)
async def test_cors_preflight(self) -> None: await check("py3.6", 200)
response = await self.client.options( await check("3.6,3.7", 200)
"/", await check("3.6,py3.7", 200)
headers={ await check("py36,py37", 200)
"Access-Control-Request-Method": "POST", await check("36", 200)
"Origin": "*", await check("3.6.4", 200)
"Access-Control-Request-Headers": "Content-Type", await check("3.4", 204)
}, await check("py3.4", 204)
) await check("py34,py36", 204)
self.assertEqual(response.status, 200) await check("34", 204)
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 @unittest_run_loop
async def test_cors_headers_present(self) -> None: async def test_blackd_line_length(self) -> None:
response = await self.client.post("/", headers={"Origin": "*"}) response = await self.client.post(
self.assertIsNotNone(response.headers.get("Access-Control-Allow-Origin")) "/", data=b'print("hello")\n', headers={blackd.LINE_LENGTH_HEADER: "7"}
self.assertIsNotNone(response.headers.get("Access-Control-Expose-Headers")) )
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"))

27
tox.ini
View File

@ -1,5 +1,5 @@
[tox] [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] [testenv]
setenv = PYTHONPATH = {toxinidir}/src setenv = PYTHONPATH = {toxinidir}/src
@ -50,6 +50,31 @@ commands =
--cov --cov-append {posargs} --cov --cov-append {posargs}
coverage report 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] [testenv:fuzz]
skip_install = True skip_install = True
deps = deps =