Ability to have `kw_only` attributes defined anywhere, as an optional feature (#559)

* Ability to disable the keyword-only order checks that forbid normal attrs after kw_only attrs

* Updated typing info

* Updated tests, and added a new test for the new ordering feature

* Pep8

* Updated docstring

* Rename to a name that reflects the usage, not the implementation

* Update tests after the rename

* Huge simplification: never check kw_only and non-init attributes order

* No more new parameter, remove from typing info

* Update tests after the simplification, and add new test
This commit is contained in:
Juan Pedro Fisanotti 2019-08-21 05:15:18 -03:00 committed by Hynek Schlawack
parent 55f71b9ec3
commit 94ee269438
2 changed files with 35 additions and 41 deletions

View File

@ -374,38 +374,24 @@ def _transform_attrs(cls, these, auto_attribs, kw_only):
attrs = AttrsClass(base_attrs + own_attrs)
# mandatory vs non-mandatory attr order only matters when they are part of
# the __init__ signature and when they aren't kw_only (which are moved to
# the end and can be mandatory or non-mandatory in any order, as they will
# be specified as keyword args anyway). Check the order of those attrs:
attrs_to_check = [
a for a in attrs if a.init is not False and a.kw_only is False
]
had_default = False
was_kw_only = False
for a in attrs:
if (
was_kw_only is False
and had_default is True
and a.default is NOTHING
and a.init is True
and a.kw_only is False
):
for a in attrs_to_check:
if had_default is True and a.default is NOTHING:
raise ValueError(
"No mandatory attributes allowed after an attribute with a "
"default value or factory. Attribute in question: %r" % (a,)
)
elif (
had_default is False
and a.default is not NOTHING
and a.init is not False
and
# Keyword-only attributes without defaults can be specified
# after keyword-only attributes with defaults.
a.kw_only is False
):
if had_default is False and a.default is not NOTHING:
had_default = True
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 (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
return _Attributes((attrs, base_attrs, base_attr_map))

View File

@ -620,27 +620,35 @@ class TestKeywordOnlyAttributes(object):
"missing 1 required keyword-only argument: 'x'"
) in e.value.args[0]
def test_conflicting_keyword_only_attributes(self):
def test_keyword_only_attributes_can_come_in_any_order(self):
"""
Raises `ValueError` if keyword-only attributes are followed by
regular (non keyword-only) attributes.
Mandatory vs non-mandatory attr order only matters when they are part
of the __init__ signature and when they aren't kw_only (which are
moved to the end and can be mandatory or non-mandatory in any order,
as they will be specified as keyword args anyway).
"""
@attr.s
class C(object):
x = attr.ib(kw_only=True)
y = attr.ib()
a = attr.ib(kw_only=True)
b = attr.ib(kw_only=True, default="b")
c = attr.ib(kw_only=True)
d = attr.ib()
e = attr.ib(default="e")
f = attr.ib(kw_only=True)
g = attr.ib(kw_only=True, default="g")
h = attr.ib(kw_only=True)
with pytest.raises(ValueError) as e:
_transform_attrs(C, None, False, False)
c = C("d", a="a", c="c", f="f", h="h")
assert (
"Non keyword-only attributes are not allowed after a "
"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)",
) == e.value.args
assert c.a == "a"
assert c.b == "b"
assert c.c == "c"
assert c.d == "d"
assert c.e == "e"
assert c.f == "f"
assert c.g == "g"
assert c.h == "h"
def test_keyword_only_attributes_allow_subclassing(self):
"""