diff --git a/mypy/plugins/attrs.py b/mypy/plugins/attrs.py index 2bbcd8d2243b..051dae18b96c 100644 --- a/mypy/plugins/attrs.py +++ b/mypy/plugins/attrs.py @@ -272,6 +272,7 @@ def attr_class_maker_callback(ctx: 'mypy.plugin.ClassDefContext', init = _get_decorator_bool_argument(ctx, 'init', True) frozen = _get_frozen(ctx, frozen_default) order = _determine_eq_order(ctx) + slots = _get_decorator_bool_argument(ctx, 'slots', False) auto_attribs = _get_decorator_optional_bool_argument(ctx, 'auto_attribs', auto_attribs_default) kw_only = _get_decorator_bool_argument(ctx, 'kw_only', False) @@ -302,6 +303,8 @@ def attr_class_maker_callback(ctx: 'mypy.plugin.ClassDefContext', return _add_attrs_magic_attribute(ctx, raw_attr_types=[info[attr.name].type for attr in attributes]) + if slots: + _add_slots(ctx, attributes) # Save the attributes so that subclasses can reuse them. ctx.cls.info.metadata['attrs'] = { @@ -727,6 +730,12 @@ def _add_attrs_magic_attribute(ctx: 'mypy.plugin.ClassDefContext', ) +def _add_slots(ctx: 'mypy.plugin.ClassDefContext', + attributes: List[Attribute]) -> None: + # Unlike `@dataclasses.dataclass`, `__slots__` is rewritten here. + ctx.cls.info.slots = {attr.name for attr in attributes} + + class MethodAdder: """Helper to add methods to a TypeInfo. diff --git a/test-data/unit/check-attr.test b/test-data/unit/check-attr.test index f8dcfd811e01..75d5e53e9815 100644 --- a/test-data/unit/check-attr.test +++ b/test-data/unit/check-attr.test @@ -1402,3 +1402,33 @@ class A: reveal_type(A.__attrs_attrs__) # N: Revealed type is "Tuple[attr.Attribute[builtins.int], attr.Attribute[builtins.str]]" [builtins fixtures/attr.pyi] + +[case testAttrsClassWithSlots] +import attr + +@attr.s(slots=True) +class A: + b: int = attr.ib() + + def __attrs_post_init__(self) -> None: + self.b = 1 + self.c = 2 # E: Trying to assign name "c" that is not in "__slots__" of type "__main__.A" + +@attr.dataclass(slots=True) +class B: + __slots__ = () # would be replaced + b: int + + def __attrs_post_init__(self) -> None: + self.b = 1 + self.c = 2 # E: Trying to assign name "c" that is not in "__slots__" of type "__main__.B" + +@attr.dataclass(slots=False) +class C: + __slots__ = () # would not be replaced + b: int + + def __attrs_post_init__(self) -> None: + self.b = 1 # E: Trying to assign name "b" that is not in "__slots__" of type "__main__.C" + self.c = 2 # E: Trying to assign name "c" that is not in "__slots__" of type "__main__.C" +[builtins fixtures/attr.pyi]