Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Restore basic functionality on 3.14[sic] #1329

Merged
merged 5 commits into from
Aug 6, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/1329.change.md
Original file line number Diff line number Diff line change
@@ -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.
14 changes: 14 additions & 0 deletions src/attr/_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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:
"""
Expand Down
8 changes: 1 addition & 7 deletions src/attr/_make.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
PY_3_8_PLUS,
PY_3_10_PLUS,
_AnnotationExtractor,
_get_annotations,
get_generic_base,
)
from .exceptions import (
Expand Down Expand Up @@ -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*.
Expand Down
5 changes: 5 additions & 0 deletions tests/test_3rd_party.py
Original file line number Diff line number Diff line change
Expand Up @@ -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``.
Expand Down
11 changes: 11 additions & 0 deletions tests/test_annotations.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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",
hynek marked this conversation as resolved.
Show resolved Hide resolved
)
def test_self_reference(self, slots):
"""
References to self class using quotes can be resolved.
Expand All @@ -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.
Expand Down
6 changes: 4 additions & 2 deletions tests/test_make.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down
6 changes: 5 additions & 1 deletion tests/test_slots.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"""
Unit tests for slots-related functionality.
"""

import functools
import pickle
import weakref
Expand All @@ -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.
Expand Down Expand Up @@ -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.
Expand Down