450 kwonly before init false (#459)

* 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
This commit is contained in:
Ryan Gabbard 2018-11-24 07:03:27 -05:00 committed by Hynek Schlawack
parent 7bfd0e4061
commit e114561a06
3 changed files with 62 additions and 5 deletions

View File

@ -0,0 +1 @@
Allow `init=False` arguments after `kw_only` arguments.

View File

@ -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

View File

@ -705,7 +705,8 @@ class TestKeywordOnlyAttributes(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)",
@ -771,6 +772,62 @@ class TestKeywordOnlyAttributes(object):
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):