E57C [attrs] Field level kw_only=False overrides kw_only=True at class lev… · python/mypy@ba41d11 · GitHub
[go: up one dir, main page]

Skip to content

Commit ba41d11

Browse files
[attrs] Field level kw_only=False overrides kw_only=True at class level (#20949)
fixes #20947 When setting kw_only=True at class level with attrs, all fields are set to be keyword only. Since attrs=v25.4.0, individual attributes with kw_only=False are treated as positional (to be consistent with dataclasses, see python-attrs/attrs#1457). With this PR, Mypy considers attributes with kw_only=False to be positional. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent cbb60d8 commit ba41d11

File tree

2 files changed

+56
-12
lines changed

2 files changed

+56
-12
lines changed

mypy/plugins/attrs.py

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -424,13 +424,14 @@ def _get_frozen(ctx: mypy.plugin.ClassDefContext, frozen_default: bool) -> bool:
424424

425425

426426
def _analyze_class(
427-
ctx: mypy.plugin.ClassDefContext, auto_attribs: bool | None, kw_only: bool
427+
ctx: mypy.plugin.ClassDefContext, auto_attribs: bool | None, class_kw_only: bool
428428
) -> list[Attribute]:
429429
"""Analyze the class body of an attr maker, its parents, and return the Attributes found.
430430
431431
auto_attribs=True means we'll generate attributes from type annotations also.
432432
auto_attribs=None means we'll detect which mode to use.
433-
kw_only=True means that all attributes created here will be keyword only args in __init__.
433+
class_kw_only=True means that all attributes created here will be keyword only args by
434+
default in __init__.
434435
"""
435436
own_attrs: dict[str, Attribute] = {}
436437
if auto_attribs is None:
@@ -439,7 +440,7 @@ def _analyze_class(
439440
# Walk the body looking for assignments and decorators.
440441
for stmt in ctx.cls.defs.body:
441442
if isinstance(stmt, AssignmentStmt):
442-
for attr in _attributes_from_assignment(ctx, stmt, auto_attribs, kw_only):
443+
for attr in _attributes_from_assignment(ctx, stmt, auto_attribs, class_kw_only):
443444
# When attrs are defined twice in the same body we want to use the 2nd definition
444445
# in the 2nd location. So remove it from the OrderedDict.
445446
# Unless it's auto_attribs in which case we want the 2nd definition in the
@@ -537,7 +538,7 @@ def _detect_auto_attribs(ctx: mypy.plugin.ClassDefContext) -> bool:
537538

538539

539540
def _attributes_from_assignment(
540-
ctx: mypy.plugin.ClassDefContext, stmt: AssignmentStmt, auto_attribs: bool, kw_only: bool
541+
ctx: mypy.plugin.ClassDefContext, stmt: AssignmentStmt, auto_attribs: bool, class_kw_only: bool
541542
) -> Iterable[Attribute]:
542543
"""Return Attribute objects that are created by this assignment.
543544
@@ -565,11 +566,13 @@ def _attributes_from_assignment(
565566
and isinstance(rvalue.callee, RefExpr)
566567
and rvalue.callee.fullname in attr_attrib_makers
567568
):
568-
attr = _attribute_from_attrib_maker(ctx, auto_attribs, kw_only, lhs, rvalue, stmt)
569+
attr = _attribute_from_attrib_maker(
570+
ctx, auto_attribs, class_kw_only, lhs, rvalue, stmt
571+
)
569572
if attr:
570573
yield attr
571574
elif auto_attribs and stmt.type and stmt.new_syntax and not is_class_var(lhs):
572-
yield _attribute_from_auto_attrib(ctx, kw_only, lhs, rvalue, stmt)
575+
yield _attribute_from_auto_attrib(ctx, class_kw_only, lhs, rvalue, stmt)
573576

574577

575578
def _cleanup_decorator(stmt: Decorator, attr_map: dict[str, Attribute]) -> None:
@@ -604,7 +607,7 @@ def _cleanup_decorator(stmt: Decorator, attr_map: dict[str, Attribute]) -> None:
604607

605608
def _attribute_from_auto_attrib(
606609
ctx: mypy.plugin.ClassDefContext,
607-
kw_only: bool,
610+
class_kw_only: bool,
608611
lhs: NameExpr,
609612
rvalue: Expression,
610613
stmt: AssignmentStmt,
@@ -615,13 +618,13 @@ def _attribute_from_auto_attrib(
615618
has_rhs = not isinstance(rvalue, TempNode)
616619
sym = ctx.cls.info.names.get(name)
617620
init_type = sym.type if sym else None
618-
return Attribute(name, None, ctx.cls.info, has_rhs, True, kw_only, None, stmt, init_type)
621+
return Attribute(name, None, ctx.cls.info, has_rhs, True, class_kw_only, None, stmt, init_type)
619622

620623

621624
def _attribute_from_attrib_maker(
622625
ctx: mypy.plugin.ClassDefContext,
623626
auto_attribs: bool,
624-
kw_only: bool,
627+
class_kw_only: bool,
625628
lhs: NameExpr,
626629
rvalue: CallExpr,
627630
stmt: AssignmentStmt,
@@ -642,9 +645,9 @@ def _attribute_from_attrib_maker(
642645

643646
# Read all the arguments from the call.
644647
init = _get_bool_argument(ctx, rvalue, "init", True)
645-
# Note: If the class decorator says kw_only=True the attribute is ignored.
646-
# See https://github.com/python-attrs/attrs/issues/481 for explanation.
647-
kw_only |= _get_bool_argument(ctx, rvalue, "kw_only", False)
648+
# The class decorator kw_only value can be overridden by the attribute value
649+
# See https://github.com/python-attrs/attrs/pull/1457
650+
kw_only = _get_bool_argument(ctx, rvalue, "kw_only", class_kw_only)
648651

649652
# TODO: Check for attr.NOTHING
650653
attr_has_default = bool(_get_argument(rvalue, "default"))

test-data/unit/check-plugin-attrs.test

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1324,6 +1324,47 @@ D(b=17)
13241324
[builtins fixtures/plugin_attrs.pyi]
13251325

13261326

1327+
[case testAttrsKwOnlyClassAttributeFalse]
1328+
from attrs import define, field
1329+
@define(kw_only=True)
1330+
class A:
1331+
a: int = field(kw_only=False)
1332+
b: int = 1
1333+
A(0)
1334+
A(1, b=3)
1335+
[builtins fixtures/plugin_attrs.pyi]
1336+
1337+
[case testAttrsKwOnlyClassAttributeFalseSubclass]
1338+
from attrs import define, field
1339+
@define(kw_only 8B92 =True)
1340+
class A:
1341+
a: int = field(kw_only=False)
1342+
b: int = 1
1343+
@define(kw_only=True)
1344+
class B(A):
1345+
c: int = field(kw_only=False)
1346+
d: int = 3
1347+
1348+
B(0, 1)
1349+
B(0, 1, b=3, d=4)
1350+
[builtins fixtures/plugin_attrs.pyi]
1351+
1352+
[case testAttrsKwOnlyClassAttributeFalseSubclassOnly]
1353+
from attrs import define, field
1354+
@define
1355+
class A:
1356+
a: int
1357+
b: int = field(kw_only=True, default=1)
1358+
@define(kw_only=True)
1359+
class B(A):
1360+
c: int = field(kw_only=False)
1361+
d: int = 3
1362+
1363+
B(0, 1)
1364+
B(0, 1, b=3, d=4)
1365+
[builtins fixtures/plugin_attrs.pyi]
1366+
1367+
13271368
[case testAttrsKwOnlySubclass]
13281369
import attr
13291370
@attr.s

0 commit comments

Comments
 (0)
0