Skip to content

Commit

Permalink
450 kwonly before init false (#459)
Browse files Browse the repository at this point in the history
* Allow init=False attributes to follow kw_only attributes

Closes #450

* Fix changelog typo

* Update reference text for exception message

* Remove type annotations from tests

* Fix long docstring lines

* Add test with literal default
  • Loading branch information
Ryan Gabbard authored and hynek committed Nov 24, 2018
1 parent 7bfd0e4 commit e114561
Show file tree
Hide file tree
Showing 3 changed files with 62 additions and 5 deletions.
1 change: 1 addition & 0 deletions changelog.d/450.change.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Allow `init=False` arguments after `kw_only` arguments.
7 changes: 3 additions & 4 deletions src/attr/_make.py
Original file line number Diff line number Diff line change
Expand Up @@ -409,12 +409,11 @@ def _transform_attrs(cls, these, auto_attribs, kw_only):
a.kw_only is False
):
had_default = True
if was_kw_only is True and a.kw_only is False:
if was_kw_only is True and a.kw_only is False and a.init is True:
raise ValueError(
"Non keyword-only attributes are not allowed after a "
"keyword-only attribute. Attribute in question: {a!r}".format(
a=a
)
"keyword-only attribute (unless they are init=False). "
"Attribute in question: {a!r}".format(a=a)
)
if was_kw_only is False and a.init is True and a.kw_only is True:
was_kw_only = True
Expand Down
59 changes: 58 additions & 1 deletion tests/test_make.py
Original file line number Diff line number Diff line change
Expand Up @@ -705,7 +705,8 @@ class C(object):

assert (
"Non keyword-only attributes are not allowed after a "
"keyword-only attribute. Attribute in question: Attribute"
"keyword-only attribute (unless they are init=False). "
"Attribute in question: Attribute"
"(name='y', default=NOTHING, validator=None, repr=True, "
"cmp=True, hash=None, init=True, metadata=mappingproxy({}), "
"type=None, converter=None, kw_only=False)",
Expand Down Expand Up @@ -771,6 +772,62 @@ class C(Base):
assert c.x == 0
assert c.y == 1

def test_init_false_attribute_after_keyword_attribute(self):
"""
A positional attribute cannot follow a `kw_only` attribute,
but an `init=False` attribute can because it won't appear
in `__init__`
"""

@attr.s
class KwArgBeforeInitFalse:
kwarg = attr.ib(kw_only=True)
non_init_function_default = attr.ib(init=False)
non_init_keyword_default = attr.ib(
init=False, default="default-by-keyword"
)

@non_init_function_default.default
def _init_to_init(self):
return self.kwarg + "b"

c = KwArgBeforeInitFalse(kwarg="a")

assert c.kwarg == "a"
assert c.non_init_function_default == "ab"
assert c.non_init_keyword_default == "default-by-keyword"

def test_init_false_attribute_after_keyword_attribute_with_inheritance(
self
):
"""
A positional attribute cannot follow a `kw_only` attribute,
but an `init=False` attribute can because it won't appear
in `__init__`. This test checks that we allow this
even when the `kw_only` attribute appears in a parent class
"""

@attr.s
class KwArgBeforeInitFalseParent:
kwarg = attr.ib(kw_only=True)

@attr.s
class KwArgBeforeInitFalseChild(KwArgBeforeInitFalseParent):
non_init_function_default = attr.ib(init=False)
non_init_keyword_default = attr.ib(
init=False, default="default-by-keyword"
)

@non_init_function_default.default
def _init_to_init(self):
return self.kwarg + "b"

c = KwArgBeforeInitFalseChild(kwarg="a")

assert c.kwarg == "a"
assert c.non_init_function_default == "ab"
assert c.non_init_keyword_default == "default-by-keyword"


@pytest.mark.skipif(not PY2, reason="PY2-specific keyword-only error behavior")
class TestKeywordOnlyAttributesOnPy2(object):
Expand Down

0 comments on commit e114561

Please sign in to comment.