Skip to content

Commit e114561

Browse files
Ryan Gabbardhynek
Ryan Gabbard
authored andcommitted
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
1 parent 7bfd0e4 commit e114561

File tree

3 files changed

+62
-5
lines changed

3 files changed

+62
-5
lines changed

changelog.d/450.change.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Allow `init=False` arguments after `kw_only` arguments.

src/attr/_make.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -409,12 +409,11 @@ def _transform_attrs(cls, these, auto_attribs, kw_only):
409409
a.kw_only is False
410410
):
411411
had_default = True
412-
if was_kw_only is True and a.kw_only is False:
412+
if was_kw_only is True and a.kw_only is False and a.init is True:
413413
raise ValueError(
414414
"Non keyword-only attributes are not allowed after a "
415-
"keyword-only attribute. Attribute in question: {a!r}".format(
416-
a=a
417-
)
415+
"keyword-only attribute (unless they are init=False). "
416+
"Attribute in question: {a!r}".format(a=a)
418417
)
419418
if was_kw_only is False and a.init is True and a.kw_only is True:
420419
was_kw_only = True

tests/test_make.py

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -705,7 +705,8 @@ class C(object):
705705

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

775+
def test_init_false_attribute_after_keyword_attribute(self):
776+
"""
777+
A positional attribute cannot follow a `kw_only` attribute,
778+
but an `init=False` attribute can because it won't appear
779+
in `__init__`
780+
"""
781+
782+
@attr.s
783+
class KwArgBeforeInitFalse:
784+
kwarg = attr.ib(kw_only=True)
785+
non_init_function_default = attr.ib(init=False)
786+
non_init_keyword_default = attr.ib(
787+
init=False, default="default-by-keyword"
788+
)
789+
790+
@non_init_function_default.default
791+
def _init_to_init(self):
792+
return self.kwarg + "b"
793+
794+
c = KwArgBeforeInitFalse(kwarg="a")
795+
796+
assert c.kwarg == "a"
797+
assert c.non_init_function_default == "ab"
798+
assert c.non_init_keyword_default == "default-by-keyword"
799+
800+
def test_init_false_attribute_after_keyword_attribute_with_inheritance(
801+
self
802+
):
803+
"""
804+
A positional attribute cannot follow a `kw_only` attribute,
805+
but an `init=False` attribute can because it won't appear
806+
in `__init__`. This test checks that we allow this
807+
even when the `kw_only` attribute appears in a parent class
808+
"""
809+
810+
@attr.s
811+
class KwArgBeforeInitFalseParent:
812+
kwarg = attr.ib(kw_only=True)
813+
814+
@attr.s
815+
class KwArgBeforeInitFalseChild(KwArgBeforeInitFalseParent):
816+
non_init_function_default = attr.ib(init=False)
817+
non_init_keyword_default = attr.ib(
818+
init=False, default="default-by-keyword"
819+
)
820+
821+
@non_init_function_default.default
822+
def _init_to_init(self):
823+
return self.kwarg + "b"
824+
825+
c = KwArgBeforeInitFalseChild(kwarg="a")
826+
827+
assert c.kwarg == "a"
828+
assert c.non_init_function_default == "ab"
829+
assert c.non_init_keyword_default == "default-by-keyword"
830+
774831

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

0 commit comments

Comments
 (0)