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

Added support for keyword-only arguments on Python 3+ [rebase] #411

Merged
merged 14 commits into from
Aug 11, 2018
Merged
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`` arguments to ``attr.ib`` and ``attr.s```, and a corresponding ``kw_only`` attribute to ``attr.Attribute``.
This change makes it possible to have a generated ``__init__`` with keyword-only arguments on Python 3, relaxing the required ordering of default and non-default valued attributes.
2 changes: 2 additions & 0 deletions changelog.d/411.change.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Added ``kw_only`` arguments to ``attr.ib`` and ``attr.s```, and a corresponding ``kw_only`` attribute to ``attr.Attribute``.
This change makes it possible to have a generated ``__init__`` with keyword-only arguments on Python 3, relaxing the required ordering of default and non-default valued attributes.
18 changes: 9 additions & 9 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, metadata=mappingproxy({}), type=None, converter=None)
Attribute(name='x', default=NOTHING, validator=None, repr=True, cmp=True, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False)


.. autofunction:: attr.make_class
Expand Down Expand Up @@ -161,9 +161,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, metadata=mappingproxy({}), type=None, converter=None), Attribute(name='y', default=NOTHING, validator=None, repr=True, cmp=True, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None))
(Attribute(name='x', default=NOTHING, validator=None, repr=True, cmp=True, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False), Attribute(name='y', default=NOTHING, validator=None, repr=True, cmp=True, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False))
>>> attr.fields(C)[1]
Attribute(name='y', default=NOTHING, validator=None, repr=True, cmp=True, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None)
Attribute(name='y', default=NOTHING, validator=None, repr=True, cmp=True, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False)
>>> attr.fields(C).y is attr.fields(C)[1]
True

Expand All @@ -178,9 +178,9 @@ Helpers
... x = attr.ib()
... y = attr.ib()
>>> attr.fields_dict(C)
{'x': Attribute(name='x', default=NOTHING, validator=None, repr=True, cmp=True, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None), 'y': Attribute(name='y', default=NOTHING, validator=None, repr=True, cmp=True, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None)}
{'x': Attribute(name='x', default=NOTHING, validator=None, repr=True, cmp=True, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False), 'y': Attribute(name='y', default=NOTHING, validator=None, repr=True, cmp=True, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False)}
>>> attr.fields_dict(C)['y']
Attribute(name='y', default=NOTHING, validator=None, repr=True, cmp=True, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None)
Attribute(name='y', default=NOTHING, validator=None, repr=True, cmp=True, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False)
>>> attr.fields_dict(C)['y'] is attr.fields(C).y
True

Expand Down Expand Up @@ -275,7 +275,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 @@ -308,11 +308,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 @@ -364,7 +364,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
71 changes: 69 additions & 2 deletions docs/examples.rst
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,73 @@ Therefore ``@attr.s`` comes with the ``repr_ns`` option to set it manually:
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)

``kw_only`` may also be specified at via ``attr.s``, and will apply to all attributes:

.. doctest::

>>> @attr.s(kw_only=True)
... class A:
... a = attr.ib()
... b = attr.ib()
>>> A(1, 2)
Traceback (most recent call last):
...
TypeError: __init__() takes 1 positional argument but 3 were given
>>> A(a=1, b=2)
A(a=1, b=2)



If you create an attribute with ``init=False``, the ``kw_only`` argument is 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 don't set ``kw_only=True``, then there's is no valid attribute ordering and 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:

Converting to Collections Types
Expand Down Expand Up @@ -352,7 +419,7 @@ You can use a decorator:
>>> 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, metadata=mappingproxy({}), type=None, converter=one), <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, metadata=mappingproxy({}), type=None, converter=one, kw_only=False), <class 'int'>, '128')
>>> C(256)
Traceback (most recent call last):
...
Expand All @@ -371,7 +438,7 @@ You can use a decorator:
>>> 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')

Check out :ref:`validators` for more details.

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, metadata=mappingproxy({}), type=None, converter=None),)
(Attribute(name='a', default=NOTHING, validator=None, repr=True, cmp=True, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False),)


.. warning::
Expand Down
8 changes: 8 additions & 0 deletions src/attr/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ class Attribute(Generic[_T]):
converter: Optional[_ConverterType[_T]]
metadata: Dict[Any, Any]
type: Optional[Type[_T]]
kw_only: bool
def __lt__(self, x: Attribute) -> bool: ...
def __le__(self, x: Attribute) -> bool: ...
def __gt__(self, x: Attribute) -> bool: ...
Expand Down Expand Up @@ -99,6 +100,7 @@ def attrib(
type: None = ...,
converter: None = ...,
factory: None = ...,
kw_only: bool = ...,
) -> Any: ...

# This form catches an explicit None or no default and infers the type from the other arguments.
Expand All @@ -115,6 +117,7 @@ def attrib(
type: Optional[Type[_T]] = ...,
converter: Optional[_ConverterType[_T]] = ...,
factory: Optional[Callable[[], _T]] = ...,
kw_only: bool = ...,
) -> _T: ...

# This form catches an explicit default argument.
Expand All @@ -131,6 +134,7 @@ def attrib(
type: Optional[Type[_T]] = ...,
converter: Optional[_ConverterType[_T]] = ...,
factory: Optional[Callable[[], _T]] = ...,
kw_only: bool = ...,
) -> _T: ...

# This form covers type=non-Type: e.g. forward references (str), Any
Expand All @@ -147,6 +151,7 @@ def attrib(
type: object = ...,
converter: Optional[_ConverterType[_T]] = ...,
factory: Optional[Callable[[], _T]] = ...,
kw_only: bool = ...,
) -> Any: ...
@overload
def attrs(
Expand All @@ -161,6 +166,7 @@ def attrs(
frozen: bool = ...,
str: bool = ...,
auto_attribs: bool = ...,
kw_only: bool = ...,
) -> _C: ...
@overload
def attrs(
Expand All @@ -175,6 +181,7 @@ def attrs(
frozen: bool = ...,
str: bool = ...,
auto_attribs: bool = ...,
kw_only: bool = ...,
) -> Callable[[_C], _C]: ...

# TODO: add support for returning NamedTuple from the mypy plugin
Expand All @@ -200,6 +207,7 @@ def make_class(
frozen: bool = ...,
str: bool = ...,
auto_attribs: bool = ...,
kw_only: bool = ...,
) -> type: ...

# _funcs --
Expand Down
Loading