From 69a7765ab6da8e54ae7df3af7532afae9c60af09 Mon Sep 17 00:00:00 2001 From: Chad Dombrova Date: Sun, 27 Aug 2017 21:49:30 -0700 Subject: [PATCH 1/5] Add support for passing a type to attr.ib() and gathering the type from PEP526-style annotations. --- docs/api.rst | 14 ++++---- docs/examples.rst | 8 ++--- docs/extending.rst | 2 +- src/attr/_make.py | 41 +++++++++++++++++------ tests/_test_annotations.py | 67 ++++++++++++++++++++++++++++++++++++++ tests/test_make.py | 15 ++++++++- tox.ini | 2 +- 7 files changed, 125 insertions(+), 24 deletions(-) create mode 100644 tests/_test_annotations.py diff --git a/docs/api.rst b/docs/api.rst index 02fa5b237..45076c2b3 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -90,7 +90,7 @@ Core ... class C(object): ... x = attr.ib() >>> C.x - Attribute(name='x', default=NOTHING, validator=None, repr=True, cmp=True, hash=None, init=True, convert=None, metadata=mappingproxy({})) + Attribute(name='x', default=NOTHING, validator=None, repr=True, cmp=True, hash=None, init=True, convert=None, metadata=mappingproxy({}), type=None) .. autofunction:: attr.make_class @@ -202,9 +202,9 @@ Helpers ... x = attr.ib() ... y = attr.ib() >>> attr.fields(C) - (Attribute(name='x', default=NOTHING, validator=None, repr=True, cmp=True, hash=None, init=True, convert=None, metadata=mappingproxy({})), Attribute(name='y', default=NOTHING, validator=None, repr=True, cmp=True, hash=None, init=True, convert=None, metadata=mappingproxy({}))) + (Attribute(name='x', default=NOTHING, validator=None, repr=True, cmp=True, hash=None, init=True, convert=None, metadata=mappingproxy({}), type=None), Attribute(name='y', default=NOTHING, validator=None, repr=True, cmp=True, hash=None, init=True, convert=None, metadata=mappingproxy({}), type=None)) >>> attr.fields(C)[1] - Attribute(name='y', default=NOTHING, validator=None, repr=True, cmp=True, hash=None, init=True, convert=None, metadata=mappingproxy({})) + Attribute(name='y', default=NOTHING, validator=None, repr=True, cmp=True, hash=None, init=True, convert=None, metadata=mappingproxy({}), type=None) >>> attr.fields(C).y is attr.fields(C)[1] True @@ -299,7 +299,7 @@ See :ref:`asdict` for examples. >>> attr.validate(i) Traceback (most recent call last): ... - TypeError: ("'x' must be (got '1' that is a ).", Attribute(name='x', default=NOTHING, validator=>, repr=True, cmp=True, hash=None, init=True), , '1') + TypeError: ("'x' must be (got '1' that is a ).", Attribute(name='x', default=NOTHING, validator=>, repr=True, cmp=True, hash=None, init=True, type=None), , '1') Validators can be globally disabled if you want to run them only in development and tests but not in production because you fear their performance impact: @@ -332,11 +332,11 @@ Validators >>> C("42") Traceback (most recent call last): ... - TypeError: ("'x' must be (got '42' that is a ).", Attribute(name='x', default=NOTHING, validator=>), , '42') + TypeError: ("'x' must be (got '42' that is a ).", Attribute(name='x', default=NOTHING, validator=>, type=None), , '42') >>> C(None) Traceback (most recent call last): ... - TypeError: ("'x' must be (got None that is a ).", Attribute(name='x', default=NOTHING, validator=>, repr=True, cmp=True, hash=None, init=True), , None) + TypeError: ("'x' must be (got None that is a ).", Attribute(name='x', default=NOTHING, validator=>, repr=True, cmp=True, hash=None, init=True, type=None), , None) .. autofunction:: attr.validators.in_ @@ -388,7 +388,7 @@ Validators >>> C("42") Traceback (most recent call last): ... - TypeError: ("'x' must be (got '42' that is a ).", Attribute(name='x', default=NOTHING, validator=>), , '42') + TypeError: ("'x' must be (got '42' that is a ).", Attribute(name='x', default=NOTHING, validator=>, type=None), , '42') >>> C(None) C(x=None) diff --git a/docs/examples.rst b/docs/examples.rst index 21c2e464e..4e888c1f0 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -368,7 +368,7 @@ This example also shows of some syntactic sugar for using the :func:`attr.valida >>> C("42") Traceback (most recent call last): ... - TypeError: ("'x' must be (got '42' that is a ).", Attribute(name='x', default=NOTHING, factory=NOTHING, validator=>), , '42') + TypeError: ("'x' must be (got '42' that is a ).", Attribute(name='x', default=NOTHING, factory=NOTHING, validator=>, type=None), , '42') Of course you can mix and match the two approaches at your convenience: @@ -386,7 +386,7 @@ Of course you can mix and match the two approaches at your convenience: >>> C("128") Traceback (most recent call last): ... - TypeError: ("'x' must be (got '128' that is a ).", Attribute(name='x', default=NOTHING, validator=[>, ], repr=True, cmp=True, hash=True, init=True, convert=None, metadata=mappingproxy({})), , '128') + TypeError: ("'x' must be (got '128' that is a ).", Attribute(name='x', default=NOTHING, validator=[>, ], repr=True, cmp=True, hash=True, init=True, convert=None, metadata=mappingproxy({}), type=None), , '128') >>> C(256) Traceback (most recent call last): ... @@ -401,7 +401,7 @@ And finally you can disable validators globally: >>> C("128") Traceback (most recent call last): ... - TypeError: ("'x' must be (got '128' that is a ).", Attribute(name='x', default=NOTHING, validator=[>, ], repr=True, cmp=True, hash=True, init=True, convert=None, metadata=mappingproxy({})), , '128') + TypeError: ("'x' must be (got '128' that is a ).", Attribute(name='x', default=NOTHING, validator=[>, ], repr=True, cmp=True, hash=True, init=True, convert=None, metadata=mappingproxy({}), type=None), , '128') Conversion @@ -514,7 +514,7 @@ Slot classes are a little different than ordinary, dictionary-backed classes: ... class C(object): ... x = attr.ib() >>> C.x - Attribute(name='x', default=NOTHING, validator=None, repr=True, cmp=True, hash=None, init=True, convert=None, metadata=mappingproxy({})) + Attribute(name='x', default=NOTHING, validator=None, repr=True, cmp=True, hash=None, init=True, convert=None, metadata=mappingproxy({}), type=None) >>> @attr.s(slots=True) ... class C(object): ... x = attr.ib() diff --git a/docs/extending.rst b/docs/extending.rst index e7a20ad93..ba612384d 100644 --- a/docs/extending.rst +++ b/docs/extending.rst @@ -17,7 +17,7 @@ So it is fairly simple to build your own decorators on top of ``attrs``: ... @attr.s ... class C(object): ... a = attr.ib() - (Attribute(name='a', default=NOTHING, validator=None, repr=True, cmp=True, hash=None, init=True, convert=None, metadata=mappingproxy({})),) + (Attribute(name='a', default=NOTHING, validator=None, repr=True, cmp=True, hash=None, init=True, convert=None, metadata=mappingproxy({}), type=None),) .. warning:: diff --git a/src/attr/_make.py b/src/attr/_make.py index 987e50be6..c3ba75aab 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -62,7 +62,7 @@ def __hash__(self): def attr(default=NOTHING, validator=None, repr=True, cmp=True, hash=None, init=True, - convert=None, metadata={}): + convert=None, metadata={}, type=None): """ Create a new attribute on a class. @@ -125,10 +125,16 @@ def attr(default=NOTHING, validator=None, value is converted before being passed to the validator, if any. :param metadata: An arbitrary mapping, to be used by third-party components. See :ref:`extending_metadata`. + :param type: The type of the attribute. In python 3.6 or greater, the + preferred method to specify the type is using a variable annotation + (see PEP-526 ). This argument is provided for backward compatibility. + Regardless of the approach used, the type will be stored on + ``Attribute.type``. .. versionchanged:: 17.1.0 *validator* can be a ``list`` now. .. versionchanged:: 17.1.0 *hash* is ``None`` and therefore mirrors *cmp* by default . + .. versionadded:: 17.3.0 *type* """ if hash is not None and hash is not True and hash is not False: raise TypeError( @@ -143,6 +149,7 @@ def attr(default=NOTHING, validator=None, init=init, convert=convert, metadata=metadata, + type=type, ) @@ -191,8 +198,11 @@ def _transform_attrs(cls, these): for name, ca in iteritems(these)] + ann = getattr(cls, '__annotations__', {}) + non_super_attrs = [ - Attribute.from_counting_attr(name=attr_name, ca=ca) + Attribute.from_counting_attr(name=attr_name, ca=ca, + type=ann.get(attr_name)) for attr_name, ca in sorted(ca_list, key=lambda e: e[1].counter) ] @@ -212,7 +222,8 @@ def _transform_attrs(cls, these): AttrsClass = _make_attr_tuple_class(cls.__name__, attr_names) cls.__attrs_attrs__ = AttrsClass(super_cls + [ - Attribute.from_counting_attr(name=attr_name, ca=ca) + Attribute.from_counting_attr(name=attr_name, ca=ca, + type=ann.get(attr_name)) for attr_name, ca in sorted(ca_list, key=lambda e: e[1].counter) ]) @@ -853,11 +864,11 @@ class Attribute(object): """ __slots__ = ( "name", "default", "validator", "repr", "cmp", "hash", "init", - "convert", "metadata", + "convert", "metadata", "type" ) def __init__(self, name, default, validator, repr, cmp, hash, init, - convert=None, metadata=None): + convert=None, metadata=None, type=None): # Cache this descriptor here to speed things up later. bound_setattr = _obj_setattr.__get__(self, Attribute) @@ -871,22 +882,31 @@ def __init__(self, name, default, validator, repr, cmp, hash, init, bound_setattr("convert", convert) bound_setattr("metadata", (metadata_proxy(metadata) if metadata else _empty_metadata_singleton)) + bound_setattr("type", type) def __setattr__(self, name, value): raise FrozenInstanceError() @classmethod - def from_counting_attr(cls, name, ca): + def from_counting_attr(cls, name, ca, type=None): + # type holds the annotated value. deal with conflicts: + if type is None: + type = ca.type + elif ca.type is not None and type is not ca.type: + raise ValueError( + "Type conflict: annotated type and given type differ: {ann} " + "is not {given}.".format(given=ca.type, ann=type) + ) inst_dict = { k: getattr(ca, k) for k in Attribute.__slots__ if k not in ( - "name", "validator", "default", + "name", "validator", "default", "type" ) # exclude methods } return cls(name=name, validator=ca._validator, default=ca._default, - **inst_dict) + type=type, **inst_dict) # Don't use _add_pickle since fields(Attribute) doesn't work def __getstate__(self): @@ -929,7 +949,7 @@ class _CountingAttr(object): likely the result of a bug like a forgotten `@attr.s` decorator. """ __slots__ = ("counter", "_default", "repr", "cmp", "hash", "init", - "metadata", "_validator", "convert") + "metadata", "_validator", "convert", "type") __attrs_attrs__ = tuple( Attribute(name=name, default=NOTHING, validator=None, repr=True, cmp=True, hash=True, init=True) @@ -942,7 +962,7 @@ class _CountingAttr(object): cls_counter = 0 def __init__(self, default, validator, repr, cmp, hash, init, convert, - metadata): + metadata, type): _CountingAttr.cls_counter += 1 self.counter = _CountingAttr.cls_counter self._default = default @@ -957,6 +977,7 @@ def __init__(self, default, validator, repr, cmp, hash, init, convert, self.init = init self.convert = convert self.metadata = metadata + self.type = type def validator(self, meth): """ diff --git a/tests/_test_annotations.py b/tests/_test_annotations.py new file mode 100644 index 000000000..7874db443 --- /dev/null +++ b/tests/_test_annotations.py @@ -0,0 +1,67 @@ +""" +Tests for python 3 type annotations. +""" + +from __future__ import absolute_import, division, print_function + +import pytest + +from attr._make import ( + attr, + attributes, +) + +import typing + + +class TestAnnotations(object): + """ + Tests for types derived from variable annotations (PEP-526). + """ + + def test_basic_annotations(self): + """ + Sets the `Attribute.type` attr from basic type annotations. + """ + @attributes + class C(object): + x: int = attr() + y = attr(type=str) + z = attr() + assert int is C.__attrs_attrs__[0].type + assert str is C.__attrs_attrs__[1].type + assert None is C.__attrs_attrs__[2].type + + def test_catches_basic_type_conflict(self): + """ + Raises ValueError if types conflict. + """ + with pytest.raises(ValueError) as e: + @attributes + class C: + x: int = attr(type=str) + assert ("Type conflict: annotated type and given type differ: " + " is not .",) == e.value.args + + def test_typing_annotations(self): + """ + Sets the `Attribute.type` attr from typing annotations. + """ + @attributes + class C(object): + x: typing.List[int] = attr() + y = attr(type=typing.Optional[str]) + + assert typing.List[int] is C.__attrs_attrs__[0].type + assert typing.Optional[str] is C.__attrs_attrs__[1].type + + def test_catches_typing_type_conflict(self): + """ + Raises ValueError if types conflict. + """ + with pytest.raises(ValueError) as e: + @attributes + class C: + x: int = attr(type=typing.List[str]) + assert ("Type conflict: annotated type and given type differ: " + " is not typing.List[str].",) == e.value.args diff --git a/tests/test_make.py b/tests/test_make.py index a9e400f5e..429d0ccf2 100644 --- a/tests/test_make.py +++ b/tests/test_make.py @@ -194,7 +194,7 @@ class C(object): "default value or factory. Attribute in question: Attribute" "(name='y', default=NOTHING, validator=None, repr=True, " "cmp=True, hash=None, init=True, convert=None, " - "metadata=mappingproxy({}))", + "metadata=mappingproxy({}), type=None)", ) == e.value.args def test_these(self): @@ -406,6 +406,19 @@ def __attrs_post_init__(self2): c = C(x=10, y=20) assert 30 == getattr(c, 'z', None) + def test_types(self): + """ + Sets the `Attribute.type` attr from type argument. + """ + @attributes + class C(object): + x = attr(type=int) + y = attr(type=str) + z = attr() + assert int is C.__attrs_attrs__[0].type + assert str is C.__attrs_attrs__[1].type + assert None is C.__attrs_attrs__[2].type + @attributes class GC(object): diff --git a/tox.ini b/tox.ini index 567a7859d..d6cf3da36 100644 --- a/tox.ini +++ b/tox.ini @@ -18,7 +18,7 @@ commands = coverage run --parallel -m pytest {posargs} [testenv:py36] deps = -rdev-requirements.txt -commands = coverage run --parallel -m pytest {posargs} +commands = coverage run --parallel -m pytest {posargs} tests/_test_annotations.py [testenv:flake8] From d10e1af95328ea9e7d7df1ea9bd15e700a1afdcc Mon Sep 17 00:00:00 2001 From: Chad Dombrova Date: Wed, 30 Aug 2017 09:36:41 -0700 Subject: [PATCH 2/5] Address review notes. --- changelog.d/239.change.rst | 3 ++ conftest.py | 6 ++++ src/attr/_make.py | 10 +++--- ...est_annotations.py => test_annotations.py} | 32 +++++++------------ tests/test_make.py | 6 ++-- tox.ini | 2 +- 6 files changed, 29 insertions(+), 30 deletions(-) create mode 100644 changelog.d/239.change.rst rename tests/{_test_annotations.py => test_annotations.py} (50%) diff --git a/changelog.d/239.change.rst b/changelog.d/239.change.rst new file mode 100644 index 000000000..af9851df2 --- /dev/null +++ b/changelog.d/239.change.rst @@ -0,0 +1,3 @@ +Added ``type`` argument to ``attr.attr()`` and corresponding ``type`` attribute to ``attr.Attribute``. +This value can be inspected by third-party tools for type checking and serialization. +In Python 3.6 or higher, the value of ``attr.Attribute.type`` can also be set using variable type annotations (see `PEP 526 `_). diff --git a/conftest.py b/conftest.py index be9968ce2..47e24c311 100644 --- a/conftest.py +++ b/conftest.py @@ -1,5 +1,6 @@ from __future__ import absolute_import, division, print_function +import sys import pytest @@ -16,3 +17,8 @@ class C(object): y = attr() return C + + +collect_ignore = [] +if sys.version_info[:2] < (3, 6): + collect_ignore.append("tests/test_annotations.py") diff --git a/src/attr/_make.py b/src/attr/_make.py index c3ba75aab..382b5b05c 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -125,7 +125,7 @@ def attr(default=NOTHING, validator=None, value is converted before being passed to the validator, if any. :param metadata: An arbitrary mapping, to be used by third-party components. See :ref:`extending_metadata`. - :param type: The type of the attribute. In python 3.6 or greater, the + :param type: The type of the attribute. In Python 3.6 or greater, the preferred method to specify the type is using a variable annotation (see PEP-526 ). This argument is provided for backward compatibility. Regardless of the approach used, the type will be stored on @@ -198,7 +198,7 @@ def _transform_attrs(cls, these): for name, ca in iteritems(these)] - ann = getattr(cls, '__annotations__', {}) + ann = getattr(cls, "__annotations__", {}) non_super_attrs = [ Attribute.from_counting_attr(name=attr_name, ca=ca, @@ -892,10 +892,10 @@ def from_counting_attr(cls, name, ca, type=None): # type holds the annotated value. deal with conflicts: if type is None: type = ca.type - elif ca.type is not None and type is not ca.type: + elif ca.type is not None: raise ValueError( - "Type conflict: annotated type and given type differ: {ann} " - "is not {given}.".format(given=ca.type, ann=type) + "Type annotation and type argument are both present: " + "{ann}, {given}.".format(given=ca.type, ann=type) ) inst_dict = { k: getattr(ca, k) diff --git a/tests/_test_annotations.py b/tests/test_annotations.py similarity index 50% rename from tests/_test_annotations.py rename to tests/test_annotations.py index 7874db443..4fd0aff17 100644 --- a/tests/_test_annotations.py +++ b/tests/test_annotations.py @@ -1,5 +1,5 @@ """ -Tests for python 3 type annotations. +Tests for PEP-526 type annotations. """ from __future__ import absolute_import, division, print_function @@ -9,6 +9,7 @@ from attr._make import ( attr, attributes, + fields ) import typing @@ -28,20 +29,20 @@ class C(object): x: int = attr() y = attr(type=str) z = attr() - assert int is C.__attrs_attrs__[0].type - assert str is C.__attrs_attrs__[1].type - assert None is C.__attrs_attrs__[2].type + assert int is fields(C).x.type + assert str is fields(C).y.type + assert None is fields(C).z.type def test_catches_basic_type_conflict(self): """ - Raises ValueError if types conflict. + Raises ValueError type is specified both ways. """ with pytest.raises(ValueError) as e: @attributes class C: - x: int = attr(type=str) - assert ("Type conflict: annotated type and given type differ: " - " is not .",) == e.value.args + x: int = attr(type=int) + assert ("Type annotation and type argument are both present: " + ", .",) == e.value.args def test_typing_annotations(self): """ @@ -52,16 +53,5 @@ class C(object): x: typing.List[int] = attr() y = attr(type=typing.Optional[str]) - assert typing.List[int] is C.__attrs_attrs__[0].type - assert typing.Optional[str] is C.__attrs_attrs__[1].type - - def test_catches_typing_type_conflict(self): - """ - Raises ValueError if types conflict. - """ - with pytest.raises(ValueError) as e: - @attributes - class C: - x: int = attr(type=typing.List[str]) - assert ("Type conflict: annotated type and given type differ: " - " is not typing.List[str].",) == e.value.args + assert typing.List[int] is fields(C).x.type + assert typing.Optional[str] is fields(C).y.type diff --git a/tests/test_make.py b/tests/test_make.py index 429d0ccf2..540663309 100644 --- a/tests/test_make.py +++ b/tests/test_make.py @@ -415,9 +415,9 @@ class C(object): x = attr(type=int) y = attr(type=str) z = attr() - assert int is C.__attrs_attrs__[0].type - assert str is C.__attrs_attrs__[1].type - assert None is C.__attrs_attrs__[2].type + assert int is fields(C).x.type + assert str is fields(C).y.type + assert None is fields(C).z.type @attributes diff --git a/tox.ini b/tox.ini index d6cf3da36..567a7859d 100644 --- a/tox.ini +++ b/tox.ini @@ -18,7 +18,7 @@ commands = coverage run --parallel -m pytest {posargs} [testenv:py36] deps = -rdev-requirements.txt -commands = coverage run --parallel -m pytest {posargs} tests/_test_annotations.py +commands = coverage run --parallel -m pytest {posargs} [testenv:flake8] From 143e89f7e6b548505960cc250c91b68474871e40 Mon Sep 17 00:00:00 2001 From: Chad Dombrova Date: Wed, 30 Aug 2017 10:06:03 -0700 Subject: [PATCH 3/5] More review notes. --- src/attr/_make.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/attr/_make.py b/src/attr/_make.py index 382b5b05c..02e1f7136 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -127,7 +127,8 @@ def attr(default=NOTHING, validator=None, components. See :ref:`extending_metadata`. :param type: The type of the attribute. In Python 3.6 or greater, the preferred method to specify the type is using a variable annotation - (see PEP-526 ). This argument is provided for backward compatibility. + (see `PEP 526 `_). + This argument is provided for backward compatibility. Regardless of the approach used, the type will be stored on ``Attribute.type``. From 1c2f76609f7e4e7b6f48fb6ffd2b2c1d143321d0 Mon Sep 17 00:00:00 2001 From: Chad Dombrova Date: Wed, 30 Aug 2017 22:44:36 -0700 Subject: [PATCH 4/5] A few more review changes. --- changelog.d/239.change.rst | 4 ++-- src/attr/_make.py | 5 ++--- tests/test_annotations.py | 4 ++-- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/changelog.d/239.change.rst b/changelog.d/239.change.rst index af9851df2..f0b9b5422 100644 --- a/changelog.d/239.change.rst +++ b/changelog.d/239.change.rst @@ -1,3 +1,3 @@ -Added ``type`` argument to ``attr.attr()`` and corresponding ``type`` attribute to ``attr.Attribute``. -This value can be inspected by third-party tools for type checking and serialization. +Added ``type`` argument to ``attr.ib()`` and corresponding ``type`` attribute to ``attr.Attribute``. +This change paves the way for automatic type checking and serialization (though as of this release attrs does not make use of it). In Python 3.6 or higher, the value of ``attr.Attribute.type`` can also be set using variable type annotations (see `PEP 526 `_). diff --git a/src/attr/_make.py b/src/attr/_make.py index 02e1f7136..672f8c5e9 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -127,7 +127,7 @@ def attr(default=NOTHING, validator=None, components. See :ref:`extending_metadata`. :param type: The type of the attribute. In Python 3.6 or greater, the preferred method to specify the type is using a variable annotation - (see `PEP 526 `_). + (see `PEP 526 `_). This argument is provided for backward compatibility. Regardless of the approach used, the type will be stored on ``Attribute.type``. @@ -895,8 +895,7 @@ def from_counting_attr(cls, name, ca, type=None): type = ca.type elif ca.type is not None: raise ValueError( - "Type annotation and type argument are both present: " - "{ann}, {given}.".format(given=ca.type, ann=type) + "Type annotation and type argument cannot both be present" ) inst_dict = { k: getattr(ca, k) diff --git a/tests/test_annotations.py b/tests/test_annotations.py index 4fd0aff17..8cba682d3 100644 --- a/tests/test_annotations.py +++ b/tests/test_annotations.py @@ -41,8 +41,8 @@ def test_catches_basic_type_conflict(self): @attributes class C: x: int = attr(type=int) - assert ("Type annotation and type argument are both present: " - ", .",) == e.value.args + assert ("Type annotation and type argument cannot " + "both be present",) == e.value.args def test_typing_annotations(self): """ From 0168ab86b26001f7caf244844154b022839b1e06 Mon Sep 17 00:00:00 2001 From: Chad Dombrova Date: Sun, 3 Sep 2017 15:39:24 -0700 Subject: [PATCH 5/5] Quick final fix to the changelog. --- changelog.d/239.change.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/changelog.d/239.change.rst b/changelog.d/239.change.rst index f0b9b5422..f50cd999f 100644 --- a/changelog.d/239.change.rst +++ b/changelog.d/239.change.rst @@ -1,3 +1,3 @@ Added ``type`` argument to ``attr.ib()`` and corresponding ``type`` attribute to ``attr.Attribute``. -This change paves the way for automatic type checking and serialization (though as of this release attrs does not make use of it). -In Python 3.6 or higher, the value of ``attr.Attribute.type`` can also be set using variable type annotations (see `PEP 526 `_). +This change paves the way for automatic type checking and serialization (though as of this release ``attrs`` does not make use of it). +In Python 3.6 or higher, the value of ``attr.Attribute.type`` can alternately be set using variable type annotations (see `PEP 526 `_).