Skip to content

Commit 123df67

Browse files
Alex Fordhynek
Alex Ford
authored andcommitted
Added support for keyword-only arguments on Python 3+ [rebase] (#411)
* Added support for keyword-only attributes. Closes #106, and closes #38 (Rebases #281) Co-authored-by: Alex Ford <[email protected]> * Add `attr.s`-level `kw_only` flag. Add `kw_only` flag to `attr.s` decorator, indicating that all class attributes should be keyword-only in __init__. Minor updates to internal interface of `Attribute` to support evolution of attributes to `kw_only` in class factory. Expand examples with `attr.s` level kw_only. * Add `kw_only` to type stubs. * Update changelog for rebased PR. Hear ye, hear ye. A duplicate PR is born. * Tidy docs from review. * Tidy code from review. * Add explicit tests of PY2 kw_only SyntaxError behavior. * Add `PythonToOldError`, raise for kw_only on PY2. * `Attribute._evolve` to `Attribute._assoc`.
1 parent 3363bb3 commit 123df67

11 files changed

+450
-46
lines changed

changelog.d/281.change.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Added ``kw_only`` arguments to ``attr.ib`` and ``attr.s```, and a corresponding ``kw_only`` attribute to ``attr.Attribute``.
2+
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.

changelog.d/411.change.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Added ``kw_only`` arguments to ``attr.ib`` and ``attr.s```, and a corresponding ``kw_only`` attribute to ``attr.Attribute``.
2+
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.

docs/api.rst

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ Core
9090
... class C(object):
9191
... x = attr.ib()
9292
>>> attr.fields(C).x
93-
Attribute(name='x', default=NOTHING, validator=None, repr=True, cmp=True, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None)
93+
Attribute(name='x', default=NOTHING, validator=None, repr=True, cmp=True, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False)
9494

9595

9696
.. autofunction:: attr.make_class
@@ -161,9 +161,9 @@ Helpers
161161
... x = attr.ib()
162162
... y = attr.ib()
163163
>>> attr.fields(C)
164-
(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))
164+
(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))
165165
>>> attr.fields(C)[1]
166-
Attribute(name='y', default=NOTHING, validator=None, repr=True, cmp=True, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None)
166+
Attribute(name='y', default=NOTHING, validator=None, repr=True, cmp=True, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False)
167167
>>> attr.fields(C).y is attr.fields(C)[1]
168168
True
169169

@@ -178,9 +178,9 @@ Helpers
178178
... x = attr.ib()
179179
... y = attr.ib()
180180
>>> attr.fields_dict(C)
181-
{'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)}
181+
{'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)}
182182
>>> attr.fields_dict(C)['y']
183-
Attribute(name='y', default=NOTHING, validator=None, repr=True, cmp=True, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None)
183+
Attribute(name='y', default=NOTHING, validator=None, repr=True, cmp=True, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False)
184184
>>> attr.fields_dict(C)['y'] is attr.fields(C).y
185185
True
186186

@@ -275,7 +275,7 @@ See :ref:`asdict` for examples.
275275
>>> attr.validate(i)
276276
Traceback (most recent call last):
277277
...
278-
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')
278+
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')
279279

280280

281281
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:
@@ -308,11 +308,11 @@ Validators
308308
>>> C("42")
309309
Traceback (most recent call last):
310310
...
311-
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')
311+
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')
312312
>>> C(None)
313313
Traceback (most recent call last):
314314
...
315-
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)
315+
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)
316316

317317
.. autofunction:: attr.validators.in_
318318

@@ -364,7 +364,7 @@ Validators
364364
>>> C("42")
365365
Traceback (most recent call last):
366366
...
367-
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')
367+
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')
368368
>>> C(None)
369369
C(x=None)
370370

docs/examples.rst

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,73 @@ Therefore ``@attr.s`` comes with the ``repr_ns`` option to set it manually:
145145
On Python 3 it overrides the implicit detection.
146146

147147

148+
Keyword-only Attributes
149+
~~~~~~~~~~~~~~~~~~~~~~~
150+
151+
When using ``attrs`` on Python 3, you can also add `keyword-only <https://docs.python.org/3/glossary.html#keyword-only-parameter>`_ attributes:
152+
153+
.. doctest::
154+
155+
>>> @attr.s
156+
... class A:
157+
... a = attr.ib(kw_only=True)
158+
>>> A()
159+
Traceback (most recent call last):
160+
...
161+
TypeError: A() missing 1 required keyword-only argument: 'a'
162+
>>> A(a=1)
163+
A(a=1)
164+
165+
``kw_only`` may also be specified at via ``attr.s``, and will apply to all attributes:
166+
167+
.. doctest::
168+
169+
>>> @attr.s(kw_only=True)
170+
... class A:
171+
... a = attr.ib()
172+
... b = attr.ib()
173+
>>> A(1, 2)
174+
Traceback (most recent call last):
175+
...
176+
TypeError: __init__() takes 1 positional argument but 3 were given
177+
>>> A(a=1, b=2)
178+
A(a=1, b=2)
179+
180+
181+
182+
If you create an attribute with ``init=False``, the ``kw_only`` argument is ignored.
183+
184+
Keyword-only attributes allow subclasses to add attributes without default values, even if the base class defines attributes with default values:
185+
186+
.. doctest::
187+
188+
>>> @attr.s
189+
... class A:
190+
... a = attr.ib(default=0)
191+
>>> @attr.s
192+
... class B(A):
193+
... b = attr.ib(kw_only=True)
194+
>>> B(b=1)
195+
B(a=0, b=1)
196+
>>> B()
197+
Traceback (most recent call last):
198+
...
199+
TypeError: B() missing 1 required keyword-only argument: 'b'
200+
201+
If you don't set ``kw_only=True``, then there's is no valid attribute ordering and you'll get an error:
202+
203+
.. doctest::
204+
205+
>>> @attr.s
206+
... class A:
207+
... a = attr.ib(default=0)
208+
>>> @attr.s
209+
... class B(A):
210+
... b = attr.ib()
211+
Traceback (most recent call last):
212+
...
213+
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)
214+
148215
.. _asdict:
149216

150217
Converting to Collections Types
@@ -352,7 +419,7 @@ You can use a decorator:
352419
>>> C("128")
353420
Traceback (most recent call last):
354421
...
355-
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')
422+
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')
356423
>>> C(256)
357424
Traceback (most recent call last):
358425
...
@@ -371,7 +438,7 @@ You can use a decorator:
371438
>>> C("42")
372439
Traceback (most recent call last):
373440
...
374-
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')
441+
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')
375442

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

docs/extending.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ So it is fairly simple to build your own decorators on top of ``attrs``:
1717
... @attr.s
1818
... class C(object):
1919
... a = attr.ib()
20-
(Attribute(name='a', default=NOTHING, validator=None, repr=True, cmp=True, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None),)
20+
(Attribute(name='a', default=NOTHING, validator=None, repr=True, cmp=True, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False),)
2121

2222

2323
.. warning::

src/attr/__init__.pyi

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ class Attribute(Generic[_T]):
5656
converter: Optional[_ConverterType[_T]]
5757
metadata: Dict[Any, Any]
5858
type: Optional[Type[_T]]
59+
kw_only: bool
5960
def __lt__(self, x: Attribute) -> bool: ...
6061
def __le__(self, x: Attribute) -> bool: ...
6162
def __gt__(self, x: Attribute) -> bool: ...
@@ -99,6 +100,7 @@ def attrib(
99100
type: None = ...,
100101
converter: None = ...,
101102
factory: None = ...,
103+
kw_only: bool = ...,
102104
) -> Any: ...
103105

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

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

136140
# This form covers type=non-Type: e.g. forward references (str), Any
@@ -147,6 +151,7 @@ def attrib(
147151
type: object = ...,
148152
converter: Optional[_ConverterType[_T]] = ...,
149153
factory: Optional[Callable[[], _T]] = ...,
154+
kw_only: bool = ...,
150155
) -> Any: ...
151156
@overload
152157
def attrs(
@@ -161,6 +166,7 @@ def attrs(
161166
frozen: bool = ...,
162167
str: bool = ...,
163168
auto_attribs: bool = ...,
169+
kw_only: bool = ...,
164170
) -> _C: ...
165171
@overload
166172
def attrs(
@@ -175,6 +181,7 @@ def attrs(
175181
frozen: bool = ...,
176182
str: bool = ...,
177183
auto_attribs: bool = ...,
184+
kw_only: bool = ...,
178185
) -> Callable[[_C], _C]: ...
179186

180187
# TODO: add support for returning NamedTuple from the mypy plugin
@@ -200,6 +207,7 @@ def make_class(
200207
frozen: bool = ...,
201208
str: bool = ...,
202209
auto_attribs: bool = ...,
210+
kw_only: bool = ...,
203211
) -> type: ...
204212

205213
# _funcs --

0 commit comments

Comments
 (0)