Skip to content

Commit 88bf6f2

Browse files
authored
Add alias support to field() in attrs plugin (#16610)
Closes #16586 CC @ikonst
1 parent 1cdeecd commit 88bf6f2

File tree

3 files changed

+49
-3
lines changed

3 files changed

+49
-3
lines changed

mypy/plugins/attrs.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ class Attribute:
105105
def __init__(
106106
self,
107107
name: str,
108+
alias: str | None,
108109
info: TypeInfo,
109110
has_default: bool,
110111
init: bool,
@@ -114,6 +115,7 @@ def __init__(
114115
init_type: Type | None,
115116
) -> None:
116117
self.name = name
118+
self.alias = alias
117119
self.info = info
118120
self.has_default = has_default
119121
self.init = init
@@ -171,12 +173,14 @@ def argument(self, ctx: mypy.plugin.ClassDefContext) -> Argument:
171173
arg_kind = ARG_OPT if self.has_default else ARG_POS
172174

173175
# Attrs removes leading underscores when creating the __init__ arguments.
174-
return Argument(Var(self.name.lstrip("_"), init_type), init_type, None, arg_kind)
176+
name = self.alias or self.name.lstrip("_")
177+
return Argument(Var(name, init_type), init_type, None, arg_kind)
175178

176179
def serialize(self) -> JsonDict:
177180
"""Serialize this object so it can be saved and restored."""
178181
return {
179182
"name": self.name,
183+
"alias": self.alias,
180184
"has_default": self.has_default,
181185
"init": self.init,
182186
"kw_only": self.kw_only,
@@ -205,6 +209,7 @@ def deserialize(
205209

206210
return Attribute(
207211
data["name"],
212+
data["alias"],
208213
info,
209214
data["has_default"],
210215
data["init"],
@@ -498,6 +503,7 @@ def _attributes_from_assignment(
498503
or if auto_attribs is enabled also like this:
499504
x: type
500505
x: type = default_value
506+
x: type = attr.ib(...)
501507
"""
502508
for lvalue in stmt.lvalues:
503509
lvalues, rvalues = _parse_assignments(lvalue, stmt)
@@ -564,7 +570,7 @@ def _attribute_from_auto_attrib(
564570
has_rhs = not isinstance(rvalue, TempNode)
565571
sym = ctx.cls.info.names.get(name)
566572
init_type = sym.type if sym else None
567-
return Attribute(name, ctx.cls.info, has_rhs, True, kw_only, None, stmt, init_type)
573+
return Attribute(name, None, ctx.cls.info, has_rhs, True, kw_only, None, stmt, init_type)
568574

569575

570576
def _attribute_from_attrib_maker(
@@ -628,9 +634,20 @@ def _attribute_from_attrib_maker(
628634
converter = convert
629635
converter_info = _parse_converter(ctx, converter)
630636

637+
# Custom alias might be defined:
638+
alias = None
639+
alias_expr = _get_argument(rvalue, "alias")
640+
if alias_expr:
641+
alias = ctx.api.parse_str_literal(alias_expr)
642+
if alias is None:
643+
ctx.api.fail(
644+
'"alias" argument to attrs field must be a string literal',
645+
rvalue,
646+
code=LITERAL_REQ,
647+
)
631648
name = unmangle(lhs.name)
632649
return Attribute(
633-
name, ctx.cls.info, attr_has_default, init, kw_only, converter_info, stmt, init_type
650+
name, alias, ctx.cls.info, attr_has_default, init, kw_only, converter_info, stmt, init_type
634651
)
635652

636653

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

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1055,6 +1055,31 @@ C()
10551055
C(_x=42) # E: Unexpected keyword argument "_x" for "C"
10561056
[builtins fixtures/list.pyi]
10571057

1058+
[case testAttrsAliasForInit]
1059+
from attrs import define, field
1060+
1061+
@define
1062+
class C1:
1063+
_x: int = field(alias="x1")
1064+
1065+
c1 = C1(x1=42)
1066+
reveal_type(c1._x) # N: Revealed type is "builtins.int"
1067+
c1.x1 # E: "C1" has no attribute "x1"
1068+
C1(_x=42) # E: Unexpected keyword argument "_x" for "C1"
1069+
1070+
alias = "x2"
1071+
@define
1072+
class C2:
1073+
_x: int = field(alias=alias) # E: "alias" argument to attrs field must be a string literal
1074+
1075+
@define
1076+
class C3:
1077+
_x: int = field(alias="_x")
1078+
1079+
c3 = C3(_x=1)
1080+
reveal_type(c3._x) # N: Revealed type is "builtins.int"
1081+
[builtins fixtures/plugin_attrs.pyi]
1082+
10581083
[case testAttrsAutoMustBeAll]
10591084
import attr
10601085
@attr.s(auto_attribs=True)

test-data/unit/lib-stub/attrs/__init__.pyi

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ def field(
7979
eq: Optional[bool] = ...,
8080
order: Optional[bool] = ...,
8181
on_setattr: Optional[_OnSetAttrArgType] = ...,
82+
alias: Optional[str] = ...,
8283
) -> Any: ...
8384

8485
# This form catches an explicit None or no default and infers the type from the
@@ -98,6 +99,7 @@ def field(
9899
eq: Optional[bool] = ...,
99100
order: Optional[bool] = ...,
100101
on_setattr: Optional[object] = ...,
102+
alias: Optional[str] = ...,
101103
) -> _T: ...
102104

103105
# This form catches an explicit default argument.
@@ -116,6 +118,7 @@ def field(
116118
eq: Optional[bool] = ...,
117119
order: Optional[bool] = ...,
118120
on_setattr: Optional[object] = ...,
121+
alias: Optional[str] = ...,
119122
) -> _T: ...
120123

121124
# This form covers type=non-Type: e.g. forward references (str), Any
@@ -134,6 +137,7 @@ def field(
134137
eq: Optional[bool] = ...,
135138
order: Optional[bool] = ...,
136139
on_setattr: Optional[object] = ...,
140+
alias: Optional[str] = ...,
137141
) -> Any: ...
138142

139143
def evolve(inst: _T, **changes: Any) -> _T: ...

0 commit comments

Comments
 (0)