Repository: python-attrs/attrs

Problem statement:
Latest release breaks hypothesis tests in CPython main branch
See https://github.com/python/cpython/issues/122686.  Pinning to "attrs<=23.2.0" - a workaround.


Hints:
I’m afk, can I have a 
[_Short, Self Contained, Correct, Example_](http://sscce.org) please?
Sorry, I didn't deep debugging (yet).  If that helps, tracebacks from above job looks like:
```
[...]
  File "/home/runner/work/cpython/cpython-builddir/hypovenv/lib/python3.14/site-packages/hypothesis/internal/conjecture/data.py", line 2466, in stop_example
    self.tags.update([structural_coverage(l) for l in labels_for_structure])
                      ~~~~~~~~~~~~~~~~~~~^^^
  File "/home/runner/work/cpython/cpython-builddir/hypovenv/lib/python3.14/site-packages/hypothesis/internal/conjecture/data.py", line 170, in structural_coverage
    return STRUCTURAL_COVERAGE_CACHE.setdefault(label, StructuralCoverageTag(label))
                                                       ~~~~~~~~~~~~~~~~~~~~~^^^^^^^
TypeError: StructuralCoverageTag.__init__() takes 1 positional argument but 2 were given
```

It's this class in Hypothesis:
https://github.com/HypothesisWorks/hypothesis/blob/3c15b34ece30825ed153a0b350f5847efb30dd79/hypothesis-python/src/hypothesis/internal/conjecture/data.py#L157-L160

Maybe hypothesis package should add pinning, but it's not clear for me from release notes that was broken here.
From looking on my phone it looks like the breakage is that we now use the official dataclass transform decorator and they’re applying is a second time. I _think_ that’s something they have to fix, this is just the price of the typing bleeding edge. Has someone opened a ticket over there?
https://github.com/HypothesisWorks/hypothesis/issues/4067
So it looks like *attrs* can't find annotation-only fields **Python 3.14 only** anymore.


It's caused by 689a0e6 and my wild guess is that it's related to [PEP 649](https://peps.python.org/pep-0649/) / [PEP 749](https://peps.python.org/pep-0749/).

Patch:
diff --git a/src/attr/_compat.py b/src/attr/_compat.py
--- a/src/attr/_compat.py
+++ b/src/attr/_compat.py
@@ -15,6 +15,7 @@
 PY_3_10_PLUS = sys.version_info[:2] >= (3, 10)
 PY_3_12_PLUS = sys.version_info[:2] >= (3, 12)
 PY_3_13_PLUS = sys.version_info[:2] >= (3, 13)
+PY_3_14_PLUS = sys.version_info[:2] >= (3, 14)
 
 
 if sys.version_info < (3, 8):
@@ -25,6 +26,19 @@
 else:
     from typing import Protocol  # noqa: F401
 
+if PY_3_14_PLUS:  # pragma: no cover
+    import annotationlib
+
+    _get_annotations = annotationlib.get_annotations
+
+else:
+
+    def _get_annotations(cls):
+        """
+        Get annotations for *cls*.
+        """
+        return cls.__dict__.get("__annotations__", {})
+
 
 class _AnnotationExtractor:
     """
diff --git a/src/attr/_make.py b/src/attr/_make.py
--- a/src/attr/_make.py
+++ b/src/attr/_make.py
@@ -23,6 +23,7 @@
     PY_3_8_PLUS,
     PY_3_10_PLUS,
     _AnnotationExtractor,
+    _get_annotations,
     get_generic_base,
 )
 from .exceptions import (
@@ -308,13 +309,6 @@ def _has_own_attribute(cls, attrib_name):
     return attrib_name in cls.__dict__
 
 
-def _get_annotations(cls):
-    """
-    Get annotations for *cls*.
-    """
-    return cls.__dict__.get("__annotations__", {})
-
-
 def _collect_base_attrs(cls, taken_attr_names):
     """
     Collect attr.ibs from base classes of *cls*, except *taken_attr_names*.


Test patch:
diff --git a/tests/test_3rd_party.py b/tests/test_3rd_party.py
--- a/tests/test_3rd_party.py
+++ b/tests/test_3rd_party.py
@@ -8,12 +8,17 @@
 
 from hypothesis import given
 
+from attr._compat import PY_3_14_PLUS
+
 from .strategies import simple_classes
 
 
 cloudpickle = pytest.importorskip("cloudpickle")
 
 
+@pytest.mark.xfail(
+    PY_3_14_PLUS, reason="cloudpickle is currently broken on 3.14."
+)
 class TestCloudpickleCompat:
     """
     Tests for compatibility with ``cloudpickle``.
diff --git a/tests/test_annotations.py b/tests/test_annotations.py
--- a/tests/test_annotations.py
+++ b/tests/test_annotations.py
@@ -12,6 +12,7 @@
 
 import attr
 
+from attr._compat import PY_3_14_PLUS
 from attr._make import _is_class_var
 from attr.exceptions import UnannotatedAttributeError
 
@@ -588,6 +589,8 @@ def test_self_reference(self, slots):
         """
         References to self class using quotes can be resolved.
         """
+        if PY_3_14_PLUS and not slots:
+            pytest.xfail("References are changing a lot in 3.14.")
 
         @attr.s(slots=slots, auto_attribs=True)
         class A:
@@ -603,6 +606,8 @@ def test_forward_reference(self, slots):
         """
         Forward references can be resolved.
         """
+        if PY_3_14_PLUS and not slots:
+            pytest.xfail("Forward references are changing a lot in 3.14.")
 
         @attr.s(slots=slots, auto_attribs=True)
         class A:
diff --git a/tests/test_make.py b/tests/test_make.py
--- a/tests/test_make.py
+++ b/tests/test_make.py
@@ -21,7 +21,7 @@
 import attr
 
 from attr import _config
-from attr._compat import PY_3_8_PLUS, PY_3_10_PLUS
+from attr._compat import PY_3_8_PLUS, PY_3_10_PLUS, PY_3_14_PLUS
 from attr._make import (
     Attribute,
     Factory,
@@ -1838,9 +1838,11 @@ class C2(C):
         assert [C2] == C.__subclasses__()
 
     @pytest.mark.skipif(not PY_3_8_PLUS, reason="cached_property is 3.8+")
+    @pytest.mark.xfail(PY_3_14_PLUS, reason="Currently broken on nightly.")
     def test_no_references_to_original_when_using_cached_property(self):
         """
-        When subclassing a slotted class and using cached property, there are no stray references to the original class.
+        When subclassing a slotted class and using cached property, there are
+        no stray references to the original class.
         """
 
         @attr.s(slots=True)
diff --git a/tests/test_slots.py b/tests/test_slots.py
--- a/tests/test_slots.py
+++ b/tests/test_slots.py
@@ -3,6 +3,7 @@
 """
 Unit tests for slots-related functionality.
 """
+
 import functools
 import pickle
 import weakref
@@ -14,7 +15,7 @@
 import attr
 import attrs
 
-from attr._compat import PY_3_8_PLUS, PYPY
+from attr._compat import PY_3_8_PLUS, PY_3_14_PLUS, PYPY
 
 
 # Pympler doesn't work on PyPy.
@@ -774,6 +775,9 @@ def f(self) -> int:
 
 
 @pytest.mark.skipif(not PY_3_8_PLUS, reason="cached_property is 3.8+")
+@pytest.mark.xfail(
+    PY_3_14_PLUS, reason="3.14 returns weird annotation for cached_properies"
+)
 def test_slots_cached_property_infers_type():
     """
     Infers type of cached property.