Skip to content

Commit

Permalink
Ability to have kw_only attributes defined anywhere, as an optional…
Browse files Browse the repository at this point in the history
… 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
  • Loading branch information
fisadev authored and hynek committed Aug 21, 2019
1 parent 55f71b9 commit 94ee269
Show file tree
Hide file tree
Showing 2 changed files with 37 additions and 43 deletions.
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

0 comments on commit 94ee269

Please sign in to comment.