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

Basic type support #239

Merged
merged 5 commits into from
Sep 17, 2017
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
14 changes: 7 additions & 7 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -299,7 +299,7 @@ See :ref:`asdict` for examples.
>>> attr.validate(i)
Traceback (most recent call last):
...
TypeError: ("'x' must be <type 'int'> (got '1' that is a <type 'str'>).", Attribute(name='x', default=NOTHING, validator=<instance_of validator for type <type 'int'>>, repr=True, cmp=True, hash=None, init=True), <type 'int'>, '1')
TypeError: ("'x' must be <type 'int'> (got '1' that is a <type 'str'>).", Attribute(name='x', default=NOTHING, validator=<instance_of validator for type <type 'int'>>, repr=True, cmp=True, hash=None, init=True, type=None), <type 'int'>, '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:
Expand Down Expand Up @@ -332,11 +332,11 @@ Validators
>>> C("42")
Traceback (most recent call last):
...
TypeError: ("'x' must be <type 'int'> (got '42' that is a <type 'str'>).", Attribute(name='x', default=NOTHING, validator=<instance_of validator for type <type 'int'>>), <type 'int'>, '42')
TypeError: ("'x' must be <type 'int'> (got '42' that is a <type 'str'>).", Attribute(name='x', default=NOTHING, validator=<instance_of validator for type <type 'int'>>, type=None), <type 'int'>, '42')
>>> C(None)
Traceback (most recent call last):
...
TypeError: ("'x' must be <type 'int'> (got None that is a <type 'NoneType'>).", Attribute(name='x', default=NOTHING, validator=<instance_of validator for type <type 'int'>>, repr=True, cmp=True, hash=None, init=True), <type 'int'>, None)
TypeError: ("'x' must be <type 'int'> (got None that is a <type 'NoneType'>).", Attribute(name='x', default=NOTHING, validator=<instance_of validator for type <type 'int'>>, repr=True, cmp=True, hash=None, init=True, type=None), <type 'int'>, None)

.. autofunction:: attr.validators.in_

Expand Down Expand Up @@ -388,7 +388,7 @@ Validators
>>> C("42")
Traceback (most recent call last):
...
TypeError: ("'x' must be <type 'int'> (got '42' that is a <type 'str'>).", Attribute(name='x', default=NOTHING, validator=<instance_of validator for type <type 'int'>>), <type 'int'>, '42')
TypeError: ("'x' must be <type 'int'> (got '42' that is a <type 'str'>).", Attribute(name='x', default=NOTHING, validator=<instance_of validator for type <type 'int'>>, type=None), <type 'int'>, '42')
>>> C(None)
C(x=None)

Expand Down
8 changes: 4 additions & 4 deletions docs/examples.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 <type 'int'> (got '42' that is a <type 'str'>).", Attribute(name='x', default=NOTHING, factory=NOTHING, validator=<instance_of validator for type <type 'int'>>), <type 'int'>, '42')
TypeError: ("'x' must be <type 'int'> (got '42' that is a <type 'str'>).", Attribute(name='x', default=NOTHING, factory=NOTHING, validator=<instance_of validator for type <type 'int'>>, type=None), <type 'int'>, '42')

Of course you can mix and match the two approaches at your convenience:

Expand All @@ -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 <class 'int'> (got '128' that is a <class 'str'>).", Attribute(name='x', default=NOTHING, validator=[<instance_of validator for type <class 'int'>>, <function fits_byte at 0x10fd7a0d0>], repr=True, cmp=True, hash=True, init=True, convert=None, metadata=mappingproxy({})), <class 'int'>, '128')
TypeError: ("'x' must be <class 'int'> (got '128' that is a <class 'str'>).", Attribute(name='x', default=NOTHING, validator=[<instance_of validator for type <class 'int'>>, <function fits_byte at 0x10fd7a0d0>], repr=True, cmp=True, hash=True, init=True, convert=None, metadata=mappingproxy({}), type=None), <class 'int'>, '128')
>>> C(256)
Traceback (most recent call last):
...
Expand All @@ -401,7 +401,7 @@ And finally you can disable validators globally:
>>> C("128")
Traceback (most recent call last):
...
TypeError: ("'x' must be <class 'int'> (got '128' that is a <class 'str'>).", Attribute(name='x', default=NOTHING, validator=[<instance_of validator for type <class 'int'>>, <function fits_byte at 0x10fd7a0d0>], repr=True, cmp=True, hash=True, init=True, convert=None, metadata=mappingproxy({})), <class 'int'>, '128')
TypeError: ("'x' must be <class 'int'> (got '128' that is a <class 'str'>).", Attribute(name='x', default=NOTHING, validator=[<instance_of validator for type <class 'int'>>, <function fits_byte at 0x10fd7a0d0>], repr=True, cmp=True, hash=True, init=True, convert=None, metadata=mappingproxy({}), type=None), <class 'int'>, '128')


Conversion
Expand Down Expand Up @@ -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()
Expand Down
2 changes: 1 addition & 1 deletion docs/extending.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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::
Expand Down
41 changes: 31 additions & 10 deletions src/attr/_make.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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

This comment was marked as spam.

preferred method to specify the type is using a variable annotation
(see PEP-526 ). This argument is provided for backward compatibility.

This comment was marked as spam.

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*

This comment was marked as spam.

This comment was marked as spam.

"""
if hash is not None and hash is not True and hash is not False:
raise TypeError(
Expand All @@ -143,6 +149,7 @@ def attr(default=NOTHING, validator=None,
init=init,
convert=convert,
metadata=metadata,
type=type,
)


Expand Down Expand Up @@ -191,8 +198,11 @@ def _transform_attrs(cls, these):
for name, ca
in iteritems(these)]

ann = getattr(cls, '__annotations__', {})

This comment was marked as spam.


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)
]
Expand All @@ -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)
])
Expand Down Expand Up @@ -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)

Expand All @@ -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(

This comment was marked as spam.

"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):
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -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):
"""
Expand Down
67 changes: 67 additions & 0 deletions tests/_test_annotations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
"""
Tests for python 3 type annotations.

This comment was marked as spam.

"""

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: "
"<class 'int'> is not <class 'str'>.",) == 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: "
"<class 'int'> is not typing.List[str].",) == e.value.args
15 changes: 14 additions & 1 deletion tests/test_make.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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

This comment was marked as spam.

assert str is C.__attrs_attrs__[1].type
assert None is C.__attrs_attrs__[2].type


@attributes
class GC(object):
Expand Down
2 changes: 1 addition & 1 deletion tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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

This comment was marked as spam.



[testenv:flake8]
Expand Down