From 88bf6f20754c1713e3eaeee8cc882a7ba6c9483d Mon Sep 17 00:00:00 2001 From: Nikita Sobolev Date: Thu, 7 Dec 2023 16:37:31 +0300 Subject: [PATCH] Add `alias` support to `field()` in `attrs` plugin (#16610) Closes https://github.com/python/mypy/issues/16586 CC @ikonst --- mypy/plugins/attrs.py | 23 +++++++++++++++++--- test-data/unit/check-plugin-attrs.test | 25 ++++++++++++++++++++++ test-data/unit/lib-stub/attrs/__init__.pyi | 4 ++++ 3 files changed, 49 insertions(+), 3 deletions(-) diff --git a/mypy/plugins/attrs.py b/mypy/plugins/attrs.py index 3ddc234a7e4a..81f96c088ecd 100644 --- a/mypy/plugins/attrs.py +++ b/mypy/plugins/attrs.py @@ -105,6 +105,7 @@ class Attribute: def __init__( self, name: str, + alias: str | None, info: TypeInfo, has_default: bool, init: bool, @@ -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 @@ -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, @@ -205,6 +209,7 @@ def deserialize( return Attribute( data["name"], + data["alias"], info, data["has_default"], data["init"], @@ -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) @@ -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( @@ -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 ) diff --git a/test-data/unit/check-plugin-attrs.test b/test-data/unit/check-plugin-attrs.test index fb5f1f9472c2..b2161b91e225 100644 --- a/test-data/unit/check-plugin-attrs.test +++ b/test-data/unit/check-plugin-attrs.test @@ -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) diff --git a/test-data/unit/lib-stub/attrs/__init__.pyi b/test-data/unit/lib-stub/attrs/__init__.pyi index 7a88170d7271..f610957a48a3 100644 --- a/test-data/unit/lib-stub/attrs/__init__.pyi +++ b/test-data/unit/lib-stub/attrs/__init__.pyi @@ -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 @@ -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. @@ -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 @@ -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: ...