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

Annotate __init__ with type hints #363

Merged
merged 4 commits into from
Apr 6, 2018
Merged
Show file tree
Hide file tree
Changes from all 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/363.change.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Generated ``__init__`` methods now have an ``__annotations__`` attribute derived from the types of the fields.
1 change: 1 addition & 0 deletions docs/examples.rst
Original file line number Diff line number Diff line change
Expand Up @@ -519,6 +519,7 @@ If you don't mind annotating *all* attributes, you can even drop the :func:`attr
>>> AutoC.cls_var
5

The generated ``__init__`` method will have an attribute called ``__annotations__`` that contains this type information.

.. warning::

Expand Down
12 changes: 9 additions & 3 deletions src/attr/_make.py
Original file line number Diff line number Diff line change
Expand Up @@ -1034,7 +1034,7 @@ def _make_init(attrs, post_init, frozen, slots, super_attr_map):
sha1.hexdigest()
)

script, globs = _attrs_to_init_script(
script, globs, annotations = _attrs_to_init_script(
attrs,
frozen,
slots,
Expand Down Expand Up @@ -1063,7 +1063,9 @@ def _make_init(attrs, post_init, frozen, slots, super_attr_map):
unique_filename,
)

return locs["__init__"]
__init__ = locs["__init__"]
__init__.__annotations__ = annotations

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

return __init__


def _add_init(cls, frozen):
Expand Down Expand Up @@ -1259,6 +1261,7 @@ def fmt_setter_with_converter(attr_name, value_var):
# This is a dictionary of names to validator and converter callables.
# Injecting this into __init__ globals lets us avoid lookups.
names_for_globals = {}
annotations = {'return': None}

for a in attrs:
if a.validator:
Expand Down Expand Up @@ -1349,6 +1352,9 @@ def fmt_setter_with_converter(attr_name, value_var):
else:
lines.append(fmt_setter(attr_name, arg_name))

if a.init is True and a.converter is None and a.type is not None:

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

annotations[arg_name] = a.type

if attrs_to_validate: # we can skip this if there are no validators.
names_for_globals["_config"] = _config
lines.append("if _config._run_validators is True:")
Expand All @@ -1368,7 +1374,7 @@ def __init__(self, {args}):
""".format(
args=", ".join(args),
lines="\n ".join(lines) if lines else "pass",
), names_for_globals
), names_for_globals, annotations


class Attribute(object):
Expand Down
71 changes: 71 additions & 0 deletions tests/test_annotations.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ class 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__ == {
'x': int,
'y': str,
'return': None,
}

def test_catches_basic_type_conflict(self):
"""
Expand All @@ -57,6 +62,11 @@ class 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__ == {
'x': typing.List[int],
'y': typing.Optional[str],
'return': None,
}

def test_only_attrs_annotations_collected(self):
"""
Expand All @@ -68,6 +78,10 @@ class C:
y: int

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

@pytest.mark.parametrize("slots", [True, False])
def test_auto_attribs(self, slots):
Expand Down Expand Up @@ -115,6 +129,15 @@ class C:
i.y = 23
assert 23 == i.y

assert C.__init__.__annotations__ == {
'a': int,
'x': typing.List[int],
'y': int,
'z': int,
'foo': typing.Any,
'return': None,
}

@pytest.mark.parametrize("slots", [True, False])
def test_auto_attribs_unannotated(self, slots):
"""
Expand Down Expand Up @@ -154,3 +177,51 @@ 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__ == {
'a': int,
'b': int,
'return': None,
}
assert C.__init__.__annotations__ == {
'a': int,
'return': None,
}

def test_converter_annotations(self):
"""
Attributes with converters don't have annotations.
"""

@attr.s(auto_attribs=True)
class A:
a: int = attr.ib(converter=int)

assert A.__init__.__annotations__ == {'return': None}

@pytest.mark.parametrize("slots", [True, False])
def test_annotations_strings(self, slots):
"""
String annotations are passed into __init__ as is.
"""
@attr.s(auto_attribs=True, slots=slots)
class C:
cls_var: 'typing.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,
}