Skip to content

Commit 6e3786c

Browse files
huzeconghynekpre-commit-ci[bot]
authored
Make kw_only=True behavior consistent with dataclasses (#1457)
* initial change * move comment * fix docs * Apply suggestions from code review Co-authored-by: Hynek Schlawack <[email protected]> * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Add changelog * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Address comments * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: Hynek Schlawack <[email protected]> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent af34987 commit 6e3786c

File tree

6 files changed

+281
-34
lines changed

6 files changed

+281
-34
lines changed

changelog.d/1457.breaking.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
Class-level `kw_only=True` behavior is now consistent with `dataclasses`.
2+
3+
Previously, a class that sets `kw_only=True` makes all attributes keyword-only,
4+
including those from base classes. If an attribute sets `kw_only=False`, that
5+
setting is ignored, and it is still made keyword-only.
6+
7+
Now, only the attributes defined in that class that doesn't explicitly set
8+
`kw_only=False` are made keyword-only.
9+
10+
This shouldn't be a problem for most users, unless you have a pattern like this:
11+
12+
```python
13+
@attrs.define(kw_only=True)
14+
class Base:
15+
a: int
16+
b: int = attrs.field(default=1, kw_only=False)
17+
18+
@attrs.define
19+
class Subclass(Base):
20+
c: int
21+
```
22+
23+
Here, we have a `kw_only=True` attrs class (`Base`) with an attribute that sets
24+
`kw_only=False` and has a default (`Base.b`), and then create a subclass (`Subclass`)
25+
with required arguments (`Subclass.c`). Previously this would work, since it
26+
would make `Base.b` keyword-only, but now this fails since `Base.b` is positional, and
27+
we have a required positional argument (`Subclass.c`) following another argument with
28+
defaults.

src/attr/__init__.pyi

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ def attrib(
176176
type: None = ...,
177177
converter: None = ...,
178178
factory: None = ...,
179-
kw_only: bool = ...,
179+
kw_only: bool | None = ...,
180180
eq: _EqOrderType | None = ...,
181181
order: _EqOrderType | None = ...,
182182
on_setattr: _OnSetAttrArgType | None = ...,
@@ -200,7 +200,7 @@ def attrib(
200200
| tuple[_ConverterType]
201201
| None = ...,
202202
factory: Callable[[], _T] | None = ...,
203-
kw_only: bool = ...,
203+
kw_only: bool | None = ...,
204204
eq: _EqOrderType | None = ...,
205205
order: _EqOrderType | None = ...,
206206
on_setattr: _OnSetAttrArgType | None = ...,
@@ -223,7 +223,7 @@ def attrib(
223223
| tuple[_ConverterType]
224224
| None = ...,
225225
factory: Callable[[], _T] | None = ...,
226-
kw_only: bool = ...,
226+
kw_only: bool | None = ...,
227227
eq: _EqOrderType | None = ...,
228228
order: _EqOrderType | None = ...,
229229
on_setattr: _OnSetAttrArgType | None = ...,
@@ -246,7 +246,7 @@ def attrib(
246246
| tuple[_ConverterType]
247247
| None = ...,
248248
factory: Callable[[], _T] | None = ...,
249-
kw_only: bool = ...,
249+
kw_only: bool | None = ...,
250250
eq: _EqOrderType | None = ...,
251251
order: _EqOrderType | None = ...,
252252
on_setattr: _OnSetAttrArgType | None = ...,

src/attr/_make.py

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ def attrib(
114114
type=None,
115115
converter=None,
116116
factory=None,
117-
kw_only=False,
117+
kw_only=None,
118118
eq=None,
119119
order=None,
120120
on_setattr=None,
@@ -157,6 +157,9 @@ def attrib(
157157
*eq*, *order*, and *cmp* also accept a custom callable
158158
.. versionchanged:: 21.1.0 *cmp* undeprecated
159159
.. versionadded:: 22.2.0 *alias*
160+
.. versionchanged:: 25.4.0
161+
*kw_only* can now be None, and its default is also changed from False to
162+
None.
160163
"""
161164
eq, eq_key, order, order_key = _determine_attrib_eq_order(
162165
cmp, eq, order, True
@@ -374,7 +377,13 @@ def _collect_base_attrs_broken(cls, taken_attr_names):
374377

375378

376379
def _transform_attrs(
377-
cls, these, auto_attribs, kw_only, collect_by_mro, field_transformer
380+
cls,
381+
these,
382+
auto_attribs,
383+
kw_only,
384+
force_kw_only,
385+
collect_by_mro,
386+
field_transformer,
378387
) -> _Attributes:
379388
"""
380389
Transform all `_CountingAttr`s on a class into `Attribute`s.
@@ -430,7 +439,8 @@ def _transform_attrs(
430439

431440
fca = Attribute.from_counting_attr
432441
own_attrs = [
433-
fca(attr_name, ca, anns.get(attr_name)) for attr_name, ca in ca_list
442+
fca(attr_name, ca, kw_only, anns.get(attr_name))
443+
for attr_name, ca in ca_list
434444
]
435445

436446
if collect_by_mro:
@@ -442,7 +452,7 @@ def _transform_attrs(
442452
cls, {a.name for a in own_attrs}
443453
)
444454

445-
if kw_only:
455+
if kw_only and force_kw_only:
446456
own_attrs = [a.evolve(kw_only=True) for a in own_attrs]
447457
base_attrs = [a.evolve(kw_only=True) for a in base_attrs]
448458

@@ -669,6 +679,7 @@ def __init__(
669679
getstate_setstate,
670680
auto_attribs,
671681
kw_only,
682+
force_kw_only,
672683
cache_hash,
673684
is_exc,
674685
collect_by_mro,
@@ -681,6 +692,7 @@ def __init__(
681692
these,
682693
auto_attribs,
683694
kw_only,
695+
force_kw_only,
684696
collect_by_mro,
685697
field_transformer,
686698
)
@@ -1352,6 +1364,7 @@ def attrs(
13521364
field_transformer=None,
13531365
match_args=True,
13541366
unsafe_hash=None,
1367+
force_kw_only=True,
13551368
):
13561369
r"""
13571370
A class decorator that adds :term:`dunder methods` according to the
@@ -1418,6 +1431,10 @@ def attrs(
14181431
If a class has an *inherited* classmethod called
14191432
``__attrs_init_subclass__``, it is executed after the class is created.
14201433
.. deprecated:: 24.1.0 *hash* is deprecated in favor of *unsafe_hash*.
1434+
.. versionchanged:: 25.4.0
1435+
*kw_only* now only applies to attributes defined in the current class,
1436+
and respects attribute-level ``kw_only=False`` settings.
1437+
.. versionadded:: 25.4.0 *force_kw_only*
14211438
"""
14221439
if repr_ns is not None:
14231440
import warnings
@@ -1464,6 +1481,7 @@ def wrap(cls):
14641481
),
14651482
auto_attribs,
14661483
kw_only,
1484+
force_kw_only,
14671485
cache_hash,
14681486
is_exc,
14691487
collect_by_mro,
@@ -2516,7 +2534,11 @@ def __setattr__(self, name, value):
25162534
raise FrozenInstanceError
25172535

25182536
@classmethod
2519-
def from_counting_attr(cls, name: str, ca: _CountingAttr, type=None):
2537+
def from_counting_attr(
2538+
cls, name: str, ca: _CountingAttr, kw_only: bool, type=None
2539+
):
2540+
# The 'kw_only' argument is the class-level setting, and is used if the
2541+
# attribute itself does not explicitly set 'kw_only'.
25202542
# type holds the annotated value. deal with conflicts:
25212543
if type is None:
25222544
type = ca.type
@@ -2535,7 +2557,7 @@ def from_counting_attr(cls, name: str, ca: _CountingAttr, type=None):
25352557
ca.metadata,
25362558
type,
25372559
ca.converter,
2538-
ca.kw_only,
2560+
kw_only if ca.kw_only is None else ca.kw_only,
25392561
ca.eq,
25402562
ca.eq_key,
25412563
ca.order,

src/attr/_next_gen.py

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ def define(
4343
on_setattr=None,
4444
field_transformer=None,
4545
match_args=True,
46+
force_kw_only=False,
4647
):
4748
r"""
4849
A class decorator that adds :term:`dunder methods` according to
@@ -214,8 +215,12 @@ def define(
214215
5. Subclasses of a frozen class are frozen too.
215216
216217
kw_only (bool):
217-
Make all attributes keyword-only in the generated ``__init__`` (if
218-
*init* is False, this parameter is ignored).
218+
Make attributes keyword-only in the generated ``__init__`` (if
219+
*init* is False, this parameter is ignored). Attributes that
220+
explicitly set ``kw_only=False`` are not affected; base class
221+
attributes are also not affected.
222+
223+
Also see *force_kw_only*.
219224
220225
weakref_slot (bool):
221226
Make instances weak-referenceable. This has no effect unless
@@ -244,6 +249,15 @@ def define(
244249
See also `issue #428
245250
<https://github.com/python-attrs/attrs/issues/428>`_.
246251
252+
force_kw_only (bool):
253+
A back-compat flag for restoring pre-25.4.0 behavior. If True and
254+
``kw_only=True``, all attributes are made keyword-only, including
255+
base class attributes, and those set to ``kw_only=False`` at the
256+
attribute level. Defaults to False.
257+
258+
See also `issue #980
259+
<https://github.com/python-attrs/attrs/issues/980>`_.
260+
247261
getstate_setstate (bool | None):
248262
.. note::
249263
@@ -319,6 +333,11 @@ def define(
319333
.. versionadded:: 24.3.0
320334
Unless already present, a ``__replace__`` method is automatically
321335
created for `copy.replace` (Python 3.13+ only).
336+
.. versionchanged:: 25.4.0
337+
*kw_only* now only applies to attributes defined in the current class,
338+
and respects attribute-level ``kw_only=False`` settings.
339+
.. versionadded:: 25.4.0
340+
Added *force_kw_only* to go back to the previous *kw_only* behavior.
322341
323342
.. note::
324343
@@ -337,6 +356,7 @@ def define(
337356
- *auto_exc=True*
338357
- *auto_detect=True*
339358
- *order=False*
359+
- *force_kw_only=False*
340360
- Some options that were only relevant on Python 2 or were kept around
341361
for backwards-compatibility have been removed.
342362
@@ -366,6 +386,7 @@ def do_it(cls, auto_attribs):
366386
on_setattr=on_setattr,
367387
field_transformer=field_transformer,
368388
match_args=match_args,
389+
force_kw_only=force_kw_only,
369390
)
370391

371392
def wrap(cls):
@@ -424,7 +445,7 @@ def field(
424445
type=None,
425446
converter=None,
426447
factory=None,
427-
kw_only=False,
448+
kw_only=None,
428449
eq=None,
429450
order=None,
430451
on_setattr=None,
@@ -550,9 +571,10 @@ def field(
550571
itself. You can use it as part of your own code or for `static type
551572
checking <types>`.
552573
553-
kw_only (bool):
574+
kw_only (bool | None):
554575
Make this attribute keyword-only in the generated ``__init__`` (if
555-
``init`` is False, this parameter is ignored).
576+
*init* is False, this parameter is ignored). If None (default),
577+
mirror the setting from `attrs.define`.
556578
557579
on_setattr (~typing.Callable | list[~typing.Callable] | None | ~typing.Literal[attrs.setters.NO_OP]):
558580
Allows to overwrite the *on_setattr* setting from `attr.s`. If left
@@ -572,6 +594,9 @@ def field(
572594
.. versionadded:: 23.1.0
573595
The *type* parameter has been re-added; mostly for `attrs.make_class`.
574596
Please note that type checkers ignore this metadata.
597+
.. versionchanged:: 25.4.0
598+
*kw_only* can now be None, and its default is also changed from False to
599+
None.
575600
576601
.. seealso::
577602

src/attrs/__init__.pyi

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ def field(
7777
metadata: Mapping[Any, Any] | None = ...,
7878
converter: None = ...,
7979
factory: None = ...,
80-
kw_only: bool = ...,
80+
kw_only: bool | None = ...,
8181
eq: bool | None = ...,
8282
order: bool | None = ...,
8383
on_setattr: _OnSetAttrArgType | None = ...,
@@ -101,7 +101,7 @@ def field(
101101
| tuple[_ConverterType]
102102
| None = ...,
103103
factory: Callable[[], _T] | None = ...,
104-
kw_only: bool = ...,
104+
kw_only: bool | None = ...,
105105
eq: _EqOrderType | None = ...,
106106
order: _EqOrderType | None = ...,
107107
on_setattr: _OnSetAttrArgType | None = ...,
@@ -124,7 +124,7 @@ def field(
124124
| tuple[_ConverterType]
125125
| None = ...,
126126
factory: Callable[[], _T] | None = ...,
127-
kw_only: bool = ...,
127+
kw_only: bool | None = ...,
128128
eq: _EqOrderType | None = ...,
129129
order: _EqOrderType | None = ...,
130130
on_setattr: _OnSetAttrArgType | None = ...,
@@ -147,7 +147,7 @@ def field(
147147
| tuple[_ConverterType]
148148
| None = ...,
149149
factory: Callable[[], _T] | None = ...,
150-
kw_only: bool = ...,
150+
kw_only: bool | None = ...,
151151
eq: _EqOrderType | None = ...,
152152
order: _EqOrderType | None = ...,
153153
on_setattr: _OnSetAttrArgType | None = ...,

0 commit comments

Comments
 (0)