Skip to content

Commit

Permalink
Add alias support to field() in attrs plugin (#16610)
Browse files Browse the repository at this point in the history
Closes #16586
CC @ikonst
  • Loading branch information
sobolevn authored Dec 7, 2023
1 parent 1cdeecd commit 88bf6f2
Show file tree
Hide file tree
Showing 3 changed files with 49 additions and 3 deletions.
23 changes: 20 additions & 3 deletions mypy/plugins/attrs.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ class Attribute:
def __init__(
self,
name: str,
alias: str | None,
info: TypeInfo,
has_default: bool,
init: bool,
Expand All @@ -114,6 +115,7 @@ def __init__(
init_type: Type | None,
) -> None:
self.name = name
self.alias = alias
self.info = info
self.has_default = has_default
self.init = init
Expand Down Expand Up @@ -171,12 +173,14 @@ def argument(self, ctx: mypy.plugin.ClassDefContext) -> Argument:
arg_kind = ARG_OPT if self.has_default else ARG_POS

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

def serialize(self) -> JsonDict:
"""Serialize this object so it can be saved and restored."""
return {
"name": self.name,
"alias": self.alias,
"has_default": self.has_default,
"init": self.init,
"kw_only": self.kw_only,
Expand Down Expand Up @@ -205,6 +209,7 @@ def deserialize(

return Attribute(
data["name"],
data["alias"],
info,
data["has_default"],
data["init"],
Expand Down Expand Up @@ -498,6 +503,7 @@ def _attributes_from_assignment(
or if auto_attribs is enabled also like this:
x: type
x: type = default_value
x: type = attr.ib(...)
"""
for lvalue in stmt.lvalues:
lvalues, rvalues = _parse_assignments(lvalue, stmt)
Expand Down Expand Up @@ -564,7 +570,7 @@ def _attribute_from_auto_attrib(
has_rhs = not isinstance(rvalue, TempNode)
sym = ctx.cls.info.names.get(name)
init_type = sym.type if sym else None
return Attribute(name, ctx.cls.info, has_rhs, True, kw_only, None, stmt, init_type)
return Attribute(name, None, ctx.cls.info, has_rhs, True, kw_only, None, stmt, init_type)


def _attribute_from_attrib_maker(
Expand Down Expand Up @@ -628,9 +634,20 @@ def _attribute_from_attrib_maker(
converter = convert
converter_info = _parse_converter(ctx, converter)

# Custom alias might be defined:
alias = None
alias_expr = _get_argument(rvalue, "alias")
if alias_expr:
alias = ctx.api.parse_str_literal(alias_expr)
if alias is None:
ctx.api.fail(
'"alias" argument to attrs field must be a string literal',
rvalue,
code=LITERAL_REQ,
)
name = unmangle(lhs.name)
return Attribute(
name, ctx.cls.info, attr_has_default, init, kw_only, converter_info, stmt, init_type
name, alias, ctx.cls.info, attr_has_default, init, kw_only, converter_info, stmt, init_type
)


Expand Down
25 changes: 25 additions & 0 deletions test-data/unit/check-plugin-attrs.test
Original file line number Diff line number Diff line change
Expand Up @@ -1055,6 +1055,31 @@ C()
C(_x=42) # E: Unexpected keyword argument "_x" for "C"
[builtins fixtures/list.pyi]

[case testAttrsAliasForInit]
from attrs import define, field

@define
class C1:
_x: int = field(alias="x1")

c1 = C1(x1=42)
reveal_type(c1._x) # N: Revealed type is "builtins.int"
c1.x1 # E: "C1" has no attribute "x1"
C1(_x=42) # E: Unexpected keyword argument "_x" for "C1"

alias = "x2"
@define
class C2:
_x: int = field(alias=alias) # E: "alias" argument to attrs field must be a string literal

@define
class C3:
_x: int = field(alias="_x")

c3 = C3(_x=1)
reveal_type(c3._x) # N: Revealed type is "builtins.int"
[builtins fixtures/plugin_attrs.pyi]

[case testAttrsAutoMustBeAll]
import attr
@attr.s(auto_attribs=True)
Expand Down
4 changes: 4 additions & 0 deletions test-data/unit/lib-stub/attrs/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ def field(
eq: Optional[bool] = ...,
order: Optional[bool] = ...,
on_setattr: Optional[_OnSetAttrArgType] = ...,
alias: Optional[str] = ...,
) -> Any: ...

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

# This form catches an explicit default argument.
Expand All @@ -116,6 +118,7 @@ def field(
eq: Optional[bool] = ...,
order: Optional[bool] = ...,
on_setattr: Optional[object] = ...,
alias: Optional[str] = ...,
) -> _T: ...

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

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

0 comments on commit 88bf6f2

Please sign in to comment.