Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 12 commits into from
Aug 21, 2019
38 changes: 12 additions & 26 deletions src/attr/_make.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))

Expand Down
42 changes: 25 additions & 17 deletions tests/test_make.py
Original file line number Diff line number Diff line change
Expand Up @@ -620,27 +620,35 @@ class C(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()

with pytest.raises(ValueError) as e:
_transform_attrs(C, None, False, False)

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

c = C("d", a="a", c="c", f="f", h="h")

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):
"""
Expand Down