Repository: python-attrs/attrs
Pull request #459 (2018-10-31 23:56:58)

450 kwonly before init false
# Pull Request Check List

This is just a reminder about the most common mistakes.  Please make sure that you tick all *appropriate* boxes.  But please read our [contribution guide](https://www.attrs.org/en/latest/contributing.html) at least once, it will save you unnecessary review cycles!

If an item doesn't apply to your pull request, **check it anyway** to make it apparent that there's nothing to do.

- [ X] Added **tests** for changed code.
- [ X] New features have been added to our [Hypothesis testing strategy](https://github.com/python-attrs/attrs/blob/master/tests/strategies.py). (**Wasn't obvious what to add, if anything**)
- [X ] Changes or additions to public APIs are reflected in our type stubs (files ending in ``.pyi``).
    - [X ] ...and used in the stub test file `tests/typing_example.py`. **No changes to public APIs**
- [ X] Updated **documentation** for changed code. **n/a**
    - [ X] New functions/classes have to be added to `docs/api.rst` by hand.
    - [ X] Changes to the signature of `@attr.s()` have to be added by hand too.
    - [ X] Changed/added classes/methods/functions have appropriate `versionadded`, `versionchanged`, or `deprecated` [directives](http://www.sphinx-doc.org/en/stable/markup/para.html#directive-versionadded).
- [ X] Documentation in `.rst` files is written using [semantic newlines](http://rhodesmill.org/brandon/2012/one-sentence-per-line/). **no docs added**
- [X ] Changes (and possible deprecations) have news fragments in [`changelog.d`](https://github.com/python-attrs/attrs/blob/master/changelog.d).

If you have *any* questions to *any* of the points above, just **submit and ask**!  This checklist is here to *help* you, not to deter you from contributing!


hynek:  I’m at a conference so I can’t review in depths. It looks good overall, but could you make sure that `attr.ib(init=False, default=42)` works too?
rgabbard: @hynek : thanks - I've added the requested test

Patch:
diff --git a/changelog.d/450.change.rst b/changelog.d/450.change.rst
new file mode 100644
index 0000000..6f1f620
--- /dev/null
+++ b/changelog.d/450.change.rst
@@ -0,0 +1,1 @@
+Allow `init=False` arguments after `kw_only` arguments.
diff --git a/src/attr/_make.py b/src/attr/_make.py
index f7fd05e..6d72df4 100644
--- a/src/attr/_make.py
+++ b/src/attr/_make.py
@@ -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


Test patch:
diff --git a/tests/test_make.py b/tests/test_make.py
index 8bd2d2b..d02c124 100644
--- a/tests/test_make.py
+++ b/tests/test_make.py
@@ -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):