Repository: python-attrs/attrs
Pull request #1033 (2022-09-28 20:10:24)

Add unstable Python 3.12 to CI allowing failures
Ref https://github.com/python-attrs/attrs/pull/1029#issuecomment-1261394538.

This is kept as a draft that is expected to be merged once all of the following are true:
* [x] Python 3.11 final is out
* [x] GitHub updated their available CPython builds to include Python 3.12-dev
* [x] The DNM (do-not-merge) tag is dropped from the PR title

webknjaz: > * [ ]  Python 3.11 final is out
> * [ ] GitHub updated their available CPython builds to include Python 3.12-dev

So the new job is failing in 6 seconds, which means that it's also okay(ish) to merge this now — it won't waste too much of the CI resources. But there would be a downside of having a red mark in some places on the UI, which is going to be annoying.
hynek: Yeah not merging until it's passing.

Also lol you've introduced yourself a conflict. ;)
webknjaz: > Also lol you've introduced yourself a conflict. ;)

Yep, I was expecting that. That's a side effect of having a habit to keep commits and PRs atomic :man_shrugging: 
webknjaz: @hynek looks like 3.12 alpha is available now 
webknjaz: I suppose coverage might've dropped independently.
webknjaz: > I'm guessing coverage.py isn't there yet

Looks like you're right.
webknjaz: @hynek it occurred to me that maybe the jobs that are allowed to fail shouldn't influence the coverage check? WDYT?
webknjaz: @hynek it's green now!

Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 80ef440..433af99 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -34,9 +34,13 @@ jobs:
           - "3.9"
           - "3.10"
           - "3.11"
+          - "~3.12.0-0"
           - "pypy-3.7"
           - "pypy-3.8"
 
+    continue-on-error: >-
+      ${{ contains(matrix.python-version, '~') && true || false }}
+
     steps:
       - name: Harden Runner
         uses: step-security/harden-runner@v2
diff --git a/src/attr/_compat.py b/src/attr/_compat.py
index cc98c88..35a85a3 100644
--- a/src/attr/_compat.py
+++ b/src/attr/_compat.py
@@ -13,6 +13,7 @@ from collections.abc import Mapping, Sequence  # noqa
 
 PYPY = platform.python_implementation() == "PyPy"
 PY310 = sys.version_info[:2] >= (3, 10)
+PY_3_12_PLUS = sys.version_info[:2] >= (3, 12)
 
 
 def just_warn(*args, **kw):
diff --git a/tox.ini b/tox.ini
index b6308ad..0a31e9c 100644
--- a/tox.ini
+++ b/tox.ini
@@ -7,11 +7,12 @@ python =
     3.9: py39
     3.10: py310, mypy
     3.11: py311
+    3.12: py312
     pypy-3: pypy3
 
 
 [tox]
-envlist = mypy,pre-commit,py36,py37,py38,py39,py310,py311,pypy3,pyright,manifest,docs,pypi-description,changelog,coverage-report
+envlist = mypy,pre-commit,py36,py37,py38,py39,py310,py311,py312,pypy3,pyright,manifest,docs,pypi-description,changelog,coverage-report
 isolated_build = True
 
 
@@ -42,7 +43,7 @@ setenv =
 commands = {[testenv:py36]commands}
 
 
-[testenv:py311]
+[testenv:py31{1,2}]
 extras = cov
 # Python 3.6+ has a number of compile-time warnings on invalid string escapes.
 # PYTHONWARNINGS=d and --no-compile below make them visible during the Tox run.


Test patch:
diff --git a/tests/test_abc.py b/tests/test_abc.py
index d5682b4..a70b317 100644
--- a/tests/test_abc.py
+++ b/tests/test_abc.py
@@ -7,7 +7,7 @@ import pytest
 
 import attrs
 
-from attr._compat import PY310
+from attr._compat import PY310, PY_3_12_PLUS
 
 
 @pytest.mark.skipif(not PY310, reason="abc.update_abstractmethods is 3.10+")
@@ -50,7 +50,11 @@ class TestUpdateAbstractMethods:
             pass
 
         assert inspect.isabstract(StillAbstract)
-        with pytest.raises(
-            TypeError, match="class StillAbstract with abstract method foo"
-        ):
+        expected_exception_message = (
+            "^Can't instantiate abstract class StillAbstract without an "
+            "implementation for abstract method 'foo'$"
+            if PY_3_12_PLUS
+            else "class StillAbstract with abstract method foo"
+        )
+        with pytest.raises(TypeError, match=expected_exception_message):
             StillAbstract()