diff --git a/mypy/plugins/attrs.py b/mypy/plugins/attrs.py index 1593c73cd2bfe..bf8cc1dd81aeb 100644 --- a/mypy/plugins/attrs.py +++ b/mypy/plugins/attrs.py @@ -960,7 +960,9 @@ def _add_slots(ctx: mypy.plugin.ClassDefContext, attributes: list[Attribute]) -> [ctx.api.named_type("builtins.str") for _ in attributes], fallback=ctx.api.named_type("builtins.tuple"), ) - add_attribute_to_class(api=ctx.api, cls=ctx.cls, name="__slots__", typ=slots_type) + add_attribute_to_class( + api=ctx.api, cls=ctx.cls, name="__slots__", typ=slots_type, is_classvar=True + ) def _add_match_args(ctx: mypy.plugin.ClassDefContext, attributes: list[Attribute]) -> None: diff --git a/mypy/plugins/dataclasses.py b/mypy/plugins/dataclasses.py index a511e714ac6b4..af1d5438547f6 100644 --- a/mypy/plugins/dataclasses.py +++ b/mypy/plugins/dataclasses.py @@ -475,6 +475,7 @@ def add_slots(self, info: TypeInfo, attributes: list[DataclassAttribute]) -> Non self._cls, "__slots__", slots_type, + is_classvar=True, overwrite_existing=slots_defined_by_plugin, ) diff --git a/mypy/semanal.py b/mypy/semanal.py index 7f961687a8aee..7ffb505710d6f 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -4648,6 +4648,7 @@ def make_name_lvalue_var( assert self.type is not None v.info = self.type v.is_initialized_in_class = True + v.is_classvar = name == "__slots__" v.allow_incompatible_override = name in ALLOW_INCOMPATIBLE_OVERRIDE if kind != LDEF: v._fullname = self.qualified_name(name) diff --git a/mypyc/irbuild/classdef.py b/mypyc/irbuild/classdef.py index 61587b46161f7..238b628a6fb21 100644 --- a/mypyc/irbuild/classdef.py +++ b/mypyc/irbuild/classdef.py @@ -329,6 +329,9 @@ def add_attr(self, lvalue: NameExpr, stmt: AssignmentStmt) -> None: # Variable declaration with no body if isinstance(stmt.rvalue, TempNode): return + # Native classes use mypyc's own layout, so don't emit __slots__ at runtime. + if lvalue.name == "__slots__": + return # Only treat marked class variables as class variables. if not (is_class_var(lvalue) or stmt.is_final_def): return diff --git a/mypyc/test-data/run-classes.test b/mypyc/test-data/run-classes.test index 8203de6feee2d..676da9c0f75d1 100644 --- a/mypyc/test-data/run-classes.test +++ b/mypyc/test-data/run-classes.test @@ -1175,6 +1175,23 @@ assert f() == 10 A.x = 200 assert f() == 200 +[case testSlotsNotEmittedAsClassVar] +class Base: + __slots__ = ("value",) + value: int + +class Child(Base): + pass + +def make_child() -> Child: + child = Child() + child.value = 1 + return child +[file driver.py] +from native import make_child + +assert make_child().value == 1 + [case testClassVarDoesNotShadowMethodGlobal] from typing import ClassVar from testutil import assertRaises diff --git a/test-data/unit/check-dataclasses.test b/test-data/unit/check-dataclasses.test index f43ac255373e6..78dc62406de0d 100644 --- a/test-data/unit/check-dataclasses.test +++ b/test-data/unit/check-dataclasses.test @@ -1514,6 +1514,18 @@ class Some: self.y = 1 # E: Trying to assign name "y" that is not in "__slots__" of type "__main__.Some" [builtins fixtures/dataclasses.pyi] +[case testDataclassGeneratedSlotsClassVariableOverride] +from dataclasses import dataclass +from typing import ClassVar + +@dataclass(slots=True) +class Base: + value: int + +class Child(Base): + __slots__: ClassVar[tuple[str, ...]] = () +[builtins fixtures/tuple.pyi] + [case testDataclassWithSlotsDef] from dataclasses import dataclass diff --git a/test-data/unit/check-plugin-attrs.test b/test-data/unit/check-plugin-attrs.test index 5e6dd4d83ce02..ad2622adae108 100644 --- a/test-data/unit/check-plugin-attrs.test +++ b/test-data/unit/check-plugin-attrs.test @@ -1745,6 +1745,19 @@ class C: self.c = 2 # E: Trying to assign name "c" that is not in "__slots__" of type "__main__.C" [builtins fixtures/plugin_attrs.pyi] +[case testAttrsGeneratedSlotsClassVariableOverride] +from typing import ClassVar + +import attrs + +@attrs.define(slots=True) +class Base: + value: int + +class Child(Base): + __slots__: ClassVar[tuple[str, ...]] = () +[builtins fixtures/plugin_attrs.pyi] + [case testAttrsClassWithSlotsDerivedFromNonSlots] import attrs diff --git a/test-data/unit/check-slots.test b/test-data/unit/check-slots.test index 25dd630e1cbed..ae8bf7d040cf2 100644 --- a/test-data/unit/check-slots.test +++ b/test-data/unit/check-slots.test @@ -85,6 +85,17 @@ A.b = 1 [builtins fixtures/tuple.pyi] +[case testSlotsClassVariableOverride] +from typing import ClassVar + +class Base: + __slots__ = () + +class Child(Base): + __slots__: ClassVar[tuple[str, ...]] = () +[builtins fixtures/tuple.pyi] + + [case testSlotsDefinitionMultipleVars1] class A: __slots__ = __fields__ = ("a", "b")