From f58093b3cdb277261432e942a5f9176a3d6ce26f Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Tue, 6 Aug 2024 12:03:06 +0200 Subject: [PATCH 1/4] Restore basic functionality on 3.14[sic] Essentially switch to PEP 649 / 749 for annotations. Some tests need to be skipped for now, but the rest is working. Fixes #1326 --- src/attr/_compat.py | 14 ++++++++++++++ src/attr/_make.py | 8 +------- tests/test_3rd_party.py | 5 +++++ tests/test_annotations.py | 11 +++++++++++ tests/test_make.py | 6 ++++-- tests/test_slots.py | 6 +++++- 6 files changed, 40 insertions(+), 10 deletions(-) diff --git a/src/attr/_compat.py b/src/attr/_compat.py index b7d7a4b86..17adbc5d5 100644 --- 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: + 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 index 0e09281a6..d7a287359 100644 --- 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*. diff --git a/tests/test_3rd_party.py b/tests/test_3rd_party.py index b2ce06c29..96ef8dc0d 100644 --- 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 index b3f7b0956..7f3722fa9 100644 --- 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 @@ -584,6 +585,11 @@ class A: assert typing.List[int] == attr.fields(A).b.type assert typing.List[int] == attr.fields(A).c.type + @pytest.mark.skipif( + PY_3_14_PLUS, + reason="Forward references are changing a lot in 3.14. " + "Passes only for slots=True", + ) def test_self_reference(self, slots): """ References to self class using quotes can be resolved. @@ -599,6 +605,11 @@ class A: assert A == attr.fields(A).a.type assert typing.Optional[A] == attr.fields(A).b.type + @pytest.mark.skipif( + PY_3_14_PLUS, + reason="Forward references are changing a lot in 3.14." + "Passes only for slots=True", + ) def test_forward_reference(self, slots): """ Forward references can be resolved. diff --git a/tests/test_make.py b/tests/test_make.py index 0ef7db648..f00fb6969 100644 --- 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, @@ -1859,9 +1859,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 index 57b7fdb02..66e54f36c 100644 --- 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. From 73d55b57bddc9fc108dd645e23833bb35f867b7a Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Tue, 6 Aug 2024 12:06:58 +0200 Subject: [PATCH 2/4] Add news fragment --- changelog.d/1329.change.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/1329.change.md diff --git a/changelog.d/1329.change.md b/changelog.d/1329.change.md new file mode 100644 index 000000000..f4ca2f9a1 --- /dev/null +++ b/changelog.d/1329.change.md @@ -0,0 +1 @@ +Restored support for PEP [649](https://peps.python.org/pep-0649/) / [749](https://peps.python.org/pep-0749/)-implementing Pythons -- currently 3.14-dev. From 9057b73b334f03a0d8d40bf9067018034726c9c0 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Tue, 6 Aug 2024 12:10:16 +0200 Subject: [PATCH 3/4] We have not 3.14 CI yet --- src/attr/_compat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/attr/_compat.py b/src/attr/_compat.py index 17adbc5d5..4c515d22e 100644 --- a/src/attr/_compat.py +++ b/src/attr/_compat.py @@ -26,7 +26,7 @@ else: from typing import Protocol # noqa: F401 -if PY_3_14_PLUS: +if PY_3_14_PLUS: # pragma: no cover import annotationlib _get_annotations = annotationlib.get_annotations From 7c4fe53fc413f7d478f1fdd98aa1e264355bb011 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Tue, 6 Aug 2024 12:54:13 +0200 Subject: [PATCH 4/4] Use imprerative xfails instead of skips --- tests/test_annotations.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/tests/test_annotations.py b/tests/test_annotations.py index 7f3722fa9..cd09a8c7e 100644 --- a/tests/test_annotations.py +++ b/tests/test_annotations.py @@ -585,15 +585,12 @@ class A: assert typing.List[int] == attr.fields(A).b.type assert typing.List[int] == attr.fields(A).c.type - @pytest.mark.skipif( - PY_3_14_PLUS, - reason="Forward references are changing a lot in 3.14. " - "Passes only for slots=True", - ) 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: @@ -605,15 +602,12 @@ class A: assert A == attr.fields(A).a.type assert typing.Optional[A] == attr.fields(A).b.type - @pytest.mark.skipif( - PY_3_14_PLUS, - reason="Forward references are changing a lot in 3.14." - "Passes only for slots=True", - ) 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: