Skip to content

Commit

Permalink
Add Python 3.10 support
Browse files Browse the repository at this point in the history
Python 3.10 has string types everywhere and that has unmasked a bunch of
bugs/edge cases in our code.

Especially the hooks code need a resolving helper for string types. I'm leaving
that for a separate PR.

Fixes #716
  • Loading branch information
hynek committed Feb 19, 2021
1 parent baa8f8c commit af34d99
Show file tree
Hide file tree
Showing 8 changed files with 85 additions and 52 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:

strategy:
matrix:
python-version: ["2.7", "3.5", "3.6", "3.7", "3.8", "3.9", "pypy2", "pypy3"]
python-version: ["2.7", "3.5", "3.6", "3.7", "3.8", "3.9", "3.10.0-alpha - 3.10", "pypy2", "pypy3"]

steps:
- uses: "actions/checkout@v2"
Expand Down
7 changes: 7 additions & 0 deletions conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,10 @@ def pytest_configure(config):
"tests/test_next_gen.py",
]
)
if sys.version_info[:2] >= (3, 10):
collect_ignore.extend(
[
"tests/test_mypy.yml",
"tests/test_hooks.py",
]
)
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",
"Topic :: Software Development :: Libraries :: Python Modules",
Expand Down
6 changes: 5 additions & 1 deletion src/attr/_make.py
Original file line number Diff line number Diff line change
Expand Up @@ -366,7 +366,11 @@ def _is_class_var(annot):
annotations which would put attrs-based classes at a performance
disadvantage compared to plain old classes.
"""
return str(annot).startswith(_classvar_prefixes)
annot = str(annot)

return annot.startswith(_classvar_prefixes) or annot[1:].startswith(
_classvar_prefixes
)


def _has_own_attribute(cls, attrib_name):
Expand Down
113 changes: 67 additions & 46 deletions tests/test_annotations.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@
Python 3.6+ only.
"""

import sys
import types
import typing

import pytest

import attr

from attr._make import _classvar_prefixes
from attr.exceptions import UnannotatedAttributeError


Expand All @@ -31,14 +31,16 @@ class C:
y = attr.ib(type=str)
z = attr.ib()

attr.resolve_types(C)

assert int is attr.fields(C).x.type
assert str is attr.fields(C).y.type
assert None is attr.fields(C).z.type
assert C.__init__.__annotations__ == {
assert {
"x": int,
"y": str,
"return": None,
}
"return": type(None),
} == typing.get_type_hints(C.__init__)

def test_catches_basic_type_conflict(self):
"""
Expand All @@ -64,13 +66,15 @@ class C:
x: typing.List[int] = attr.ib()
y = attr.ib(type=typing.Optional[str])

attr.resolve_types(C)

assert typing.List[int] is attr.fields(C).x.type
assert typing.Optional[str] is attr.fields(C).y.type
assert C.__init__.__annotations__ == {
assert {
"x": typing.List[int],
"y": typing.Optional[str],
"return": None,
}
"return": type(None),
} == typing.get_type_hints(C.__init__)

def test_only_attrs_annotations_collected(self):
"""
Expand All @@ -82,11 +86,13 @@ class C:
x: typing.List[int] = attr.ib()
y: int

attr.resolve_types(C)

assert 1 == len(attr.fields(C))
assert C.__init__.__annotations__ == {
assert {
"x": typing.List[int],
"return": None,
}
"return": type(None),
} == typing.get_type_hints(C.__init__)

@pytest.mark.parametrize("slots", [True, False])
def test_auto_attribs(self, slots):
Expand All @@ -111,6 +117,8 @@ class C:
assert "a" in attr_names # just double check that the set works
assert "cls_var" not in attr_names

attr.resolve_types(C)

assert int == attr.fields(C).a.type

assert attr.Factory(list) == attr.fields(C).x.default
Expand All @@ -135,14 +143,14 @@ class C:
i.y = 23
assert 23 == i.y

assert C.__init__.__annotations__ == {
assert {
"a": int,
"x": typing.List[int],
"y": int,
"z": int,
"foo": typing.Any,
"return": None,
}
"foo": typing.Optional[typing.Any],
"return": type(None),
} == typing.get_type_hints(C.__init__)

@pytest.mark.parametrize("slots", [True, False])
def test_auto_attribs_unannotated(self, slots):
Expand Down Expand Up @@ -186,13 +194,21 @@ class C(A):
assert "B(a=1, b=2)" == repr(B())
assert "C(a=1)" == repr(C())

assert A.__init__.__annotations__ == {"a": int, "return": None}
assert B.__init__.__annotations__ == {
attr.resolve_types(A)
attr.resolve_types(B)
attr.resolve_types(C)

assert {"a": int, "return": type(None)} == typing.get_type_hints(
A.__init__
)
assert {
"a": int,
"b": int,
"return": None,
}
assert C.__init__.__annotations__ == {"a": int, "return": None}
"return": type(None),
} == typing.get_type_hints(B.__init__)
assert {"a": int, "return": type(None)} == typing.get_type_hints(
C.__init__
)

def test_converter_annotations(self):
"""
Expand All @@ -207,7 +223,9 @@ def int2str(x: int) -> str:
class A:
a = attr.ib(converter=int2str)

assert A.__init__.__annotations__ == {"a": int, "return": None}
assert {"a": int, "return": type(None)} == typing.get_type_hints(
A.__init__
)

def int2str_(x: int, y: str = ""):
return str(x)
Expand All @@ -216,7 +234,9 @@ def int2str_(x: int, y: str = ""):
class A:
a = attr.ib(converter=int2str_)

assert A.__init__.__annotations__ == {"a": int, "return": None}
assert {"a": int, "return": type(None)} == typing.get_type_hints(
A.__init__
)

def test_converter_attrib_annotations(self):
"""
Expand Down Expand Up @@ -382,30 +402,41 @@ def noop():

assert attr.converters.optional(noop).__annotations__ == {}

@pytest.mark.xfail(
sys.version_info[:2] == (3, 6), reason="Does not work on 3.6."
)
@pytest.mark.parametrize("slots", [True, False])
@pytest.mark.parametrize("classvar", _classvar_prefixes)
def test_annotations_strings(self, slots, classvar):
def test_annotations_strings(self, slots):
"""
String annotations are passed into __init__ as is.
It fails on 3.6 due to a bug in Python.
"""
import typing as t

from typing import ClassVar

@attr.s(auto_attribs=True, slots=slots)
class C:
cls_var: classvar + "[int]" = 23
cls_var1: "typing.ClassVar[int]" = 23
cls_var2: "ClassVar[int]" = 23
cls_var3: "t.ClassVar[int]" = 23
a: "int"
x: "typing.List[int]" = attr.Factory(list)
y: "int" = 2
z: "int" = attr.ib(default=3)
foo: "typing.Any" = None

assert C.__init__.__annotations__ == {
"a": "int",
"x": "typing.List[int]",
"y": "int",
"z": "int",
"foo": "typing.Any",
"return": None,
}
attr.resolve_types(C, locals(), globals())

assert {
"a": int,
"x": typing.List[int],
"y": int,
"z": int,
"foo": typing.Optional[typing.Any],
"return": type(None),
} == typing.get_type_hints(C.__init__)

def test_keyword_only_auto_attribs(self):
"""
Expand Down Expand Up @@ -487,10 +518,6 @@ class C:
y = attr.ib(type=str)
z = attr.ib()

assert "int" == attr.fields(C).x.type
assert str is attr.fields(C).y.type
assert None is attr.fields(C).z.type

attr.resolve_types(C)

assert int is attr.fields(C).x.type
Expand All @@ -509,10 +536,6 @@ class A:
b: typing.List["int"]
c: "typing.List[int]"

assert typing.List[int] == attr.fields(A).a.type
assert typing.List["int"] == attr.fields(A).b.type
assert "typing.List[int]" == attr.fields(A).c.type

# Note: I don't have to pass globals and locals here because
# int is a builtin and will be available in any scope.
attr.resolve_types(A)
Expand Down Expand Up @@ -549,9 +572,6 @@ class A:
a: "A"
b: typing.Optional["A"] # noqa: will resolve below

assert "A" == attr.fields(A).a.type
assert typing.Optional["A"] == attr.fields(A).b.type

attr.resolve_types(A, globals(), locals())

assert A == attr.fields(A).a.type
Expand All @@ -571,10 +591,11 @@ class A:
class B:
a: A

assert typing.List["B"] == attr.fields(A).a.type
assert A == attr.fields(B).a.type

attr.resolve_types(A, globals(), locals())
attr.resolve_types(B, globals(), locals())

assert typing.List[B] == attr.fields(A).a.type
assert A == attr.fields(B).a.type

assert typing.List[B] == attr.fields(A).a.type
assert A == attr.fields(B).a.type
Expand Down
3 changes: 1 addition & 2 deletions tests/test_dunders.py
Original file line number Diff line number Diff line change
Expand Up @@ -624,9 +624,8 @@ def test_init(self, slots, frozen):
with pytest.raises(TypeError) as e:
C(a=1, b=2)

assert (
assert e.value.args[0].endswith(
"__init__() got an unexpected keyword argument 'a'"
== e.value.args[0]
)

@given(booleans(), booleans())
Expand Down
2 changes: 1 addition & 1 deletion tests/test_funcs.py
Original file line number Diff line number Diff line change
Expand Up @@ -552,7 +552,7 @@ def test_unknown(self, C):
else:
expected = "__init__() got an unexpected keyword argument 'aaaa'"

assert (expected,) == e.value.args
assert e.value.args[0].endswith(expected)

def test_validator_failure(self):
"""
Expand Down
3 changes: 2 additions & 1 deletion tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,13 @@ python =
3.7: py37, docs
3.8: py38, lint, manifest, typing, changelog
3.9: py39
3.10: py310
pypy2: pypy2
pypy3: pypy3


[tox]
envlist = typing,lint,py27,py35,py36,py37,py38,py39,pypy,pypy3,manifest,docs,pypi-description,changelog,coverage-report
envlist = typing,lint,py27,py35,py36,py37,py38,py39,py310,pypy,pypy3,manifest,docs,pypi-description,changelog,coverage-report
isolated_build = True


Expand Down

0 comments on commit af34d99

Please sign in to comment.