Skip to content

Added support for keyword-only arguments on Python 3+ #281

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

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions changelog.d/281.change.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Added ``kw_only`` argument to ``attr.ib`` and corresponding ``kw_only`` attribute to ``attr.Attribute``.
This change makes it possible to have a generated ``__init__`` with keyword-only arguments on Python 3.
14 changes: 7 additions & 7 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ Core
... class C(object):
... x = attr.ib()
>>> attr.fields(C).x
Attribute(name='x', default=NOTHING, validator=None, repr=True, cmp=True, hash=None, init=True, convert=None, metadata=mappingproxy({}), type=None)
Attribute(name='x', default=NOTHING, validator=None, repr=True, cmp=True, hash=None, init=True, convert=None, metadata=mappingproxy({}), type=None, kw_only=False)


.. autofunction:: attr.make_class
Expand Down Expand Up @@ -202,9 +202,9 @@ Helpers
... x = attr.ib()
... y = attr.ib()
>>> attr.fields(C)
(Attribute(name='x', default=NOTHING, validator=None, repr=True, cmp=True, hash=None, init=True, convert=None, metadata=mappingproxy({}), type=None), Attribute(name='y', default=NOTHING, validator=None, repr=True, cmp=True, hash=None, init=True, convert=None, metadata=mappingproxy({}), type=None))
(Attribute(name='x', default=NOTHING, validator=None, repr=True, cmp=True, hash=None, init=True, convert=None, metadata=mappingproxy({}), type=None, kw_only=False), Attribute(name='y', default=NOTHING, validator=None, repr=True, cmp=True, hash=None, init=True, convert=None, metadata=mappingproxy({}), type=None, kw_only=False))
>>> attr.fields(C)[1]
Attribute(name='y', default=NOTHING, validator=None, repr=True, cmp=True, hash=None, init=True, convert=None, metadata=mappingproxy({}), type=None)
Attribute(name='y', default=NOTHING, validator=None, repr=True, cmp=True, hash=None, init=True, convert=None, metadata=mappingproxy({}), type=None, kw_only=False)
>>> attr.fields(C).y is attr.fields(C)[1]
True

Expand Down Expand Up @@ -299,7 +299,7 @@ See :ref:`asdict` for examples.
>>> attr.validate(i)
Traceback (most recent call last):
...
TypeError: ("'x' must be <type 'int'> (got '1' that is a <type 'str'>).", Attribute(name='x', default=NOTHING, validator=<instance_of validator for type <type 'int'>>, repr=True, cmp=True, hash=None, init=True, type=None), <type 'int'>, '1')
TypeError: ("'x' must be <type 'int'> (got '1' that is a <type 'str'>).", Attribute(name='x', default=NOTHING, validator=<instance_of validator for type <type 'int'>>, repr=True, cmp=True, hash=None, init=True, type=None, kw_only=False), <type 'int'>, '1')


Validators can be globally disabled if you want to run them only in development and tests but not in production because you fear their performance impact:
Expand Down Expand Up @@ -332,11 +332,11 @@ Validators
>>> C("42")
Traceback (most recent call last):
...
TypeError: ("'x' must be <type 'int'> (got '42' that is a <type 'str'>).", Attribute(name='x', default=NOTHING, validator=<instance_of validator for type <type 'int'>>, type=None), <type 'int'>, '42')
TypeError: ("'x' must be <type 'int'> (got '42' that is a <type 'str'>).", Attribute(name='x', default=NOTHING, validator=<instance_of validator for type <type 'int'>>, type=None, kw_only=False), <type 'int'>, '42')
>>> C(None)
Traceback (most recent call last):
...
TypeError: ("'x' must be <type 'int'> (got None that is a <type 'NoneType'>).", Attribute(name='x', default=NOTHING, validator=<instance_of validator for type <type 'int'>>, repr=True, cmp=True, hash=None, init=True, type=None), <type 'int'>, None)
TypeError: ("'x' must be <type 'int'> (got None that is a <type 'NoneType'>).", Attribute(name='x', default=NOTHING, validator=<instance_of validator for type <type 'int'>>, repr=True, cmp=True, hash=None, init=True, type=None, kw_only=False), <type 'int'>, None)

.. autofunction:: attr.validators.in_

Expand Down Expand Up @@ -388,7 +388,7 @@ Validators
>>> C("42")
Traceback (most recent call last):
...
TypeError: ("'x' must be <type 'int'> (got '42' that is a <type 'str'>).", Attribute(name='x', default=NOTHING, validator=<instance_of validator for type <type 'int'>>, type=None), <type 'int'>, '42')
TypeError: ("'x' must be <type 'int'> (got '42' that is a <type 'str'>).", Attribute(name='x', default=NOTHING, validator=<instance_of validator for type <type 'int'>>, type=None, kw_only=False), <type 'int'>, '42')
>>> C(None)
C(x=None)

Expand Down
55 changes: 52 additions & 3 deletions docs/examples.rst
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,55 @@ Therefore ``@attr.s`` comes with the ``repr_ns`` option to set it manually:
``repr_ns`` works on both Python 2 and 3.
On Python 3 it overrides the implicit detection.

Keyword-only Attributes
~~~~~~~~~~~~~~~~~~~~~~~

When using ``attrs`` on Python 3, you can also add `keyword-only <https://docs.python.org/3/glossary.html#keyword-only-parameter>`_ attributes:

.. doctest::

>>> @attr.s
... class A:
... a = attr.ib(kw_only=True)
>>> A()
Traceback (most recent call last):
...
TypeError: A() missing 1 required keyword-only argument: 'a'
>>> A(a=1)
A(a=1)

If you create an attribute with ``init=False``, ``kw_only`` argument is simply ignored.

Keyword-only attributes allow subclasses to add attributes without default values, even if the base class defines attributes with default values:

.. doctest::

>>> @attr.s
... class A:
... a = attr.ib(default=0)
>>> @attr.s
... class B(A):
... b = attr.ib(kw_only=True)
>>> B(b=1)
B(a=0, b=1)
>>> B()
Traceback (most recent call last):
...
TypeError: B() missing 1 required keyword-only argument: 'b'

If you omit ``kw_only`` or specify ``kw_only=False``, then you'll get an error:

.. doctest::

>>> @attr.s
... class A:
... a = attr.ib(default=0)
>>> @attr.s
... class B(A):
... b = attr.ib()
Traceback (most recent call last):
...
ValueError: No mandatory attributes allowed after an attribute with a default value or factory. Attribute in question: Attribute(name='b', default=NOTHING, validator=None, repr=True, cmp=True, hash=None, init=True, convert=None, metadata=mappingproxy({}), type=None, kw_only=False)

.. _asdict:

Expand Down Expand Up @@ -368,7 +417,7 @@ This example also shows of some syntactic sugar for using the :func:`attr.valida
>>> C("42")
Traceback (most recent call last):
...
TypeError: ("'x' must be <type 'int'> (got '42' that is a <type 'str'>).", Attribute(name='x', default=NOTHING, factory=NOTHING, validator=<instance_of validator for type <type 'int'>>, type=None), <type 'int'>, '42')
TypeError: ("'x' must be <type 'int'> (got '42' that is a <type 'str'>).", Attribute(name='x', default=NOTHING, factory=NOTHING, validator=<instance_of validator for type <type 'int'>>, type=None, kw_only=False), <type 'int'>, '42')

Of course you can mix and match the two approaches at your convenience:

Expand All @@ -386,7 +435,7 @@ Of course you can mix and match the two approaches at your convenience:
>>> C("128")
Traceback (most recent call last):
...
TypeError: ("'x' must be <class 'int'> (got '128' that is a <class 'str'>).", Attribute(name='x', default=NOTHING, validator=[<instance_of validator for type <class 'int'>>, <function fits_byte at 0x10fd7a0d0>], repr=True, cmp=True, hash=True, init=True, convert=None, metadata=mappingproxy({}), type=None), <class 'int'>, '128')
TypeError: ("'x' must be <class 'int'> (got '128' that is a <class 'str'>).", Attribute(name='x', default=NOTHING, validator=[<instance_of validator for type <class 'int'>>, <function fits_byte at 0x10fd7a0d0>], repr=True, cmp=True, hash=True, init=True, convert=None, metadata=mappingproxy({}), type=None, kw_only=False), <class 'int'>, '128')
>>> C(256)
Traceback (most recent call last):
...
Expand All @@ -401,7 +450,7 @@ And finally you can disable validators globally:
>>> C("128")
Traceback (most recent call last):
...
TypeError: ("'x' must be <class 'int'> (got '128' that is a <class 'str'>).", Attribute(name='x', default=NOTHING, validator=[<instance_of validator for type <class 'int'>>, <function fits_byte at 0x10fd7a0d0>], repr=True, cmp=True, hash=True, init=True, convert=None, metadata=mappingproxy({}), type=None), <class 'int'>, '128')
TypeError: ("'x' must be <class 'int'> (got '128' that is a <class 'str'>).", Attribute(name='x', default=NOTHING, validator=[<instance_of validator for type <class 'int'>>, <function fits_byte at 0x10fd7a0d0>], repr=True, cmp=True, hash=True, init=True, convert=None, metadata=mappingproxy({}), type=None, kw_only=False), <class 'int'>, '128')


Conversion
Expand Down
2 changes: 1 addition & 1 deletion docs/extending.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ So it is fairly simple to build your own decorators on top of ``attrs``:
... @attr.s
... class C(object):
... a = attr.ib()
(Attribute(name='a', default=NOTHING, validator=None, repr=True, cmp=True, hash=None, init=True, convert=None, metadata=mappingproxy({}), type=None),)
(Attribute(name='a', default=NOTHING, validator=None, repr=True, cmp=True, hash=None, init=True, convert=None, metadata=mappingproxy({}), type=None, kw_only=False),)


.. warning::
Expand Down
75 changes: 56 additions & 19 deletions src/attr/_make.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ def __hash__(self):

def attrib(default=NOTHING, validator=None,
repr=True, cmp=True, hash=None, init=True,
convert=None, metadata={}, type=None):
convert=None, metadata={}, type=None, kw_only=False):
"""
Create a new attribute on a class.

Expand Down Expand Up @@ -130,11 +130,15 @@ def attrib(default=NOTHING, validator=None,
This argument is provided for backward compatibility.
Regardless of the approach used, the type will be stored on
``Attribute.type``.
:param kw_only: Make this attribute keyword-only (Python 3+)
in the generated ``__init__`` (if ``init`` is ``False``, this
parameter is simply ignored).

.. versionchanged:: 17.1.0 *validator* can be a ``list`` now.
.. versionchanged:: 17.1.0
*hash* is ``None`` and therefore mirrors *cmp* by default .
.. versionadded:: 17.3.0 *type*
.. versionadded:: 17.3.0 *kw_only*
"""
if hash is not None and hash is not True and hash is not False:
raise TypeError(
Expand All @@ -150,6 +154,7 @@ def attrib(default=NOTHING, validator=None,
convert=convert,
metadata=metadata,
type=type,
kw_only=kw_only,
)


Expand Down Expand Up @@ -257,17 +262,31 @@ def _transform_attrs(cls, these):
)

had_default = False
was_kw_only = False
for a in attrs:
if had_default is True and a.default is NOTHING and a.init is True:
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):
raise ValueError(
"No mandatory attributes allowed after an attribute with a "
"default value or factory. Attribute in question: {a!r}"
.format(a=a)
)
elif had_default is False and \
a.default is not NOTHING and \
a.init is not False:
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):
had_default = True
if was_kw_only is True and a.kw_only is False:
raise ValueError(
"Non keyword-only attributes are not allowed after a "
"keyword-only attribute. 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, super_attrs))

Expand Down Expand Up @@ -913,6 +932,7 @@ def fmt_setter_with_converter(attr_name, value_var):
}

args = []
kw_only_args = []
attrs_to_validate = []

# This is a dictionary of names to validator and converter callables.
Expand Down Expand Up @@ -960,19 +980,25 @@ def fmt_setter_with_converter(attr_name, value_var):
.format(attr_name=attr_name)
))
elif a.default is not NOTHING and not has_factory:
args.append(
"{arg_name}=attr_dict['{attr_name}'].default".format(
arg_name=arg_name,
attr_name=attr_name,
)
arg = "{arg_name}=attr_dict['{attr_name}'].default".format(
arg_name=arg_name,
attr_name=attr_name,
)
if a.kw_only:
kw_only_args.append(arg)
else:
args.append(arg)
if a.convert is not None:
lines.append(fmt_setter_with_converter(attr_name, arg_name))
names_for_globals[_init_convert_pat.format(a.name)] = a.convert
else:
lines.append(fmt_setter(attr_name, arg_name))
elif has_factory:
args.append("{arg_name}=NOTHING".format(arg_name=arg_name))
arg = "{arg_name}=NOTHING".format(arg_name=arg_name)
if a.kw_only:
kw_only_args.append(arg)
else:
args.append(arg)
lines.append("if {arg_name} is not NOTHING:"
.format(arg_name=arg_name))
init_factory_name = _init_factory_pat.format(a.name)
Expand All @@ -994,7 +1020,10 @@ def fmt_setter_with_converter(attr_name, value_var):
))
names_for_globals[init_factory_name] = a.default.factory
else:
args.append(arg_name)
if a.kw_only:
kw_only_args.append(arg_name)
else:
args.append(arg_name)
if a.convert is not None:
lines.append(fmt_setter_with_converter(attr_name, arg_name))
names_for_globals[_init_convert_pat.format(a.name)] = a.convert
Expand All @@ -1014,11 +1043,17 @@ def fmt_setter_with_converter(attr_name, value_var):
if post_init:
lines.append("self.__attrs_post_init__()")

args = ", ".join(args)
if kw_only_args:
args += "{leading_comma}*, {kw_only_args}".format(
leading_comma=", " if args else "",
kw_only_args=", ".join(kw_only_args)
)
return """\
def __init__(self, {args}):
{lines}
""".format(
args=", ".join(args),
args=args,
lines="\n ".join(lines) if lines else "pass",
), names_for_globals

Expand All @@ -1033,11 +1068,11 @@ class Attribute(object):
"""
__slots__ = (
"name", "default", "validator", "repr", "cmp", "hash", "init",
"convert", "metadata", "type"
"convert", "metadata", "type", "kw_only"
)

def __init__(self, name, default, validator, repr, cmp, hash, init,
convert=None, metadata=None, type=None):
convert=None, metadata=None, type=None, kw_only=False):
# Cache this descriptor here to speed things up later.
bound_setattr = _obj_setattr.__get__(self, Attribute)

Expand All @@ -1052,6 +1087,7 @@ def __init__(self, name, default, validator, repr, cmp, hash, init,
bound_setattr("metadata", (metadata_proxy(metadata) if metadata
else _empty_metadata_singleton))
bound_setattr("type", type)
bound_setattr("kw_only", kw_only)

def __setattr__(self, name, value):
raise FrozenInstanceError()
Expand Down Expand Up @@ -1117,20 +1153,20 @@ class _CountingAttr(object):
likely the result of a bug like a forgotten `@attr.s` decorator.
"""
__slots__ = ("counter", "_default", "repr", "cmp", "hash", "init",
"metadata", "_validator", "convert", "type")
"metadata", "_validator", "convert", "type", "kw_only")
__attrs_attrs__ = tuple(
Attribute(name=name, default=NOTHING, validator=None,
repr=True, cmp=True, hash=True, init=True)
repr=True, cmp=True, hash=True, init=True, kw_only=False)
for name
in ("counter", "_default", "repr", "cmp", "hash", "init",)
) + (
Attribute(name="metadata", default=None, validator=None,
repr=True, cmp=True, hash=False, init=True),
repr=True, cmp=True, hash=False, init=True, kw_only=False),
)
cls_counter = 0

def __init__(self, default, validator, repr, cmp, hash, init, convert,
metadata, type):
metadata, type, kw_only):
_CountingAttr.cls_counter += 1
self.counter = _CountingAttr.cls_counter
self._default = default
Expand All @@ -1146,6 +1182,7 @@ def __init__(self, default, validator, repr, cmp, hash, init, convert,
self.convert = convert
self.metadata = metadata
self.type = type
self.kw_only = kw_only

def validator(self, meth):
"""
Expand Down
Loading