From 1a5e9e0ce57957552a2219e2ea82a34b3d5d21ef Mon Sep 17 00:00:00 2001 From: MarthalaJagruthiReddy <132251392+MarthalaJagruthiReddy@users.noreply.github.com> Date: Sat, 5 Sep 2026 10:40:56 -0700 Subject: [PATCH 1/5] Forward class keyword arguments in mypyc Fixes #21938 by forwarding class-definition keywords to __init_subclass__ for native classes and to the metaclass for non-native classes. Adds regression coverage for both compiler paths. --- mypyc/irbuild/classdef.py | 43 +++++++++++++++++++++++++++++++++++---- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/mypyc/irbuild/classdef.py b/mypyc/irbuild/classdef.py index 61587b46161f7..1c072bc77c21e 100644 --- a/mypyc/irbuild/classdef.py +++ b/mypyc/irbuild/classdef.py @@ -7,6 +7,7 @@ from typing import Final from mypy.nodes import ( + ARG_NAMED, ARG_POS, EXCLUDED_ENUM_ATTRIBUTES, TYPE_VAR_TUPLE_KIND, @@ -267,6 +268,8 @@ def class_body_obj(self) -> Value | None: def create_non_ext_info(self) -> NonExtClassInfo: non_ext_bases = populate_non_ext_bases(self.builder, self.cdef) non_ext_metaclass = find_non_ext_metaclass(self.builder, self.cdef, non_ext_bases) + # Class header expressions are evaluated before invoking __prepare__. + self.class_keyword_values = load_class_keyword_values(self.builder, self.cdef) non_ext_dict = setup_non_ext_dict( self.builder, self.cdef, non_ext_metaclass, non_ext_bases ) @@ -287,7 +290,9 @@ def add_attr(self, lvalue: NameExpr, stmt: AssignmentStmt) -> None: def finalize(self, ir: ClassIR) -> None: # Dynamically create the class via the type constructor - non_ext_class = load_non_ext_class(self.builder, ir, self.non_ext, self.cdef.line) + non_ext_class = load_non_ext_class( + self.builder, ir, self.non_ext, self.class_keyword_values, self.cdef.line + ) non_ext_class = load_decorated_class(self.builder, self.cdef, non_ext_class) # Try to avoid contention when using free threading. @@ -318,6 +323,8 @@ def __init__(self, builder: IRBuilder, cdef: ClassDef) -> None: super().__init__(builder, cdef) # If the class is not decorated, generate an extension class for it. self.type_obj: Value = allocate_class(builder, cdef) + # Class header expressions are evaluated before the class body runs. + self.class_keyword_values = load_class_keyword_values(builder, cdef) def class_body_obj(self) -> Value | None: return self.type_obj @@ -342,7 +349,14 @@ def add_attr(self, lvalue: NameExpr, stmt: AssignmentStmt) -> None: def finalize(self, ir: ClassIR) -> None: # Call __init_subclass__ after class attributes have been set - self.builder.call_c(py_init_subclass_op, [self.type_obj], self.cdef.line) + class_kwargs = self.builder.call_c(dict_new_op, [], self.cdef.line) + for name, value in self.class_keyword_values: + self.builder.call_c( + exact_dict_set_item_op, + [class_kwargs, self.builder.load_str(name), value], + self.cdef.line, + ) + self.builder.call_c(py_init_subclass_op, [self.type_obj, class_kwargs], self.cdef.line) # Under separate compilation, prepare.py pre-registers the decl iff # the class has its own default attribute assignments to emit, so we @@ -904,18 +918,39 @@ def gen_glue_ne_method(builder: IRBuilder, cls: ClassIR, line: int) -> None: def load_non_ext_class( - builder: IRBuilder, ir: ClassIR, non_ext: NonExtClassInfo, line: int + builder: IRBuilder, + ir: ClassIR, + non_ext: NonExtClassInfo, + class_keyword_values: list[tuple[str, Value]], + line: int, ) -> Value: cls_name = builder.load_str(ir.name) add_dunders_to_non_ext_dict(builder, non_ext, line) + args = [cls_name, non_ext.bases, non_ext.dict] + arg_kinds = [ARG_POS] * len(args) + arg_names: list[str | None] = [None] * len(args) + for name, value in class_keyword_values: + args.append(value) + arg_kinds.append(ARG_NAMED) + arg_names.append(name) + class_type_obj = builder.py_call( - non_ext.metaclass, [cls_name, non_ext.bases, non_ext.dict], line + non_ext.metaclass, args, line, arg_kinds=arg_kinds, arg_names=arg_names ) return class_type_obj +def load_class_keyword_values(builder: IRBuilder, cdef: ClassDef) -> list[tuple[str, Value]]: + """Evaluate class definition keyword arguments, excluding ``metaclass``.""" + return [ + (name, builder.accept(value)) + for name, value in cdef.keywords.items() + if name != "metaclass" + ] + + def load_decorated_class(builder: IRBuilder, cdef: ClassDef, type_obj: Value) -> Value: """Apply class decorators to create a decorated (non-extension) class object. From 8b033eda1f85502ef90db3a35e5790211ce19619 Mon Sep 17 00:00:00 2001 From: MarthalaJagruthiReddy <132251392+MarthalaJagruthiReddy@users.noreply.github.com> Date: Sat, 5 Sep 2026 10:41:48 -0700 Subject: [PATCH 2/5] Pass class keywords through mypyc runtime Update the runtime declaration and C implementation so native classes can forward keyword arguments to __init_subclass__. --- mypyc/lib-rt/CPy.h | 2 +- mypyc/lib-rt/misc_ops.c | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/mypyc/lib-rt/CPy.h b/mypyc/lib-rt/CPy.h index 502c8c3bc1ff9..115d1a946e2aa 100644 --- a/mypyc/lib-rt/CPy.h +++ b/mypyc/lib-rt/CPy.h @@ -1001,7 +1001,7 @@ PyObject *CPyType_FromTemplate(PyObject *template_, PyObject *CPyType_FromTemplateWrapper(PyObject *template_, PyObject *orig_bases, PyObject *modname); -bool CPy_InitSubclass(PyObject *type); +bool CPy_InitSubclass(PyObject *type, PyObject *kwds); int CPyDataclass_SleightOfHand(PyObject *dataclass_dec, PyObject *tp, PyObject *dict, PyObject *annotations, PyObject *dataclass_type); diff --git a/mypyc/lib-rt/misc_ops.c b/mypyc/lib-rt/misc_ops.c index 621237063ae08..728c3eff45f17 100644 --- a/mypyc/lib-rt/misc_ops.c +++ b/mypyc/lib-rt/misc_ops.c @@ -347,8 +347,8 @@ PyObject *CPyType_FromTemplate(PyObject *template, // Call __init_subclass__ on the appropriate base class of type. // This is separated from CPyType_FromTemplate so that class attributes // can be set before __init_subclass__ is called. -bool CPy_InitSubclass(PyObject *type) { - if (init_subclass((PyTypeObject *)type, NULL)) { +bool CPy_InitSubclass(PyObject *type, PyObject *kwds) { + if (init_subclass((PyTypeObject *)type, kwds)) { return false; } return true; From 1d1def2a188ff69bd07f520150dc20829947bed8 Mon Sep 17 00:00:00 2001 From: MarthalaJagruthiReddy <132251392+MarthalaJagruthiReddy@users.noreply.github.com> Date: Sat, 5 Sep 2026 10:42:13 -0700 Subject: [PATCH 3/5] Add keyword argument to init_subclass primitive Expose the kwargs dictionary as an argument to the native __init_subclass__ operation. --- mypyc/primitives/misc_ops.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypyc/primitives/misc_ops.py b/mypyc/primitives/misc_ops.py index 7b78b61f50e26..ad6722db8cc5b 100644 --- a/mypyc/primitives/misc_ops.py +++ b/mypyc/primitives/misc_ops.py @@ -269,7 +269,7 @@ # Call __init_subclass__ on a type. Separated from CPyType_FromTemplate # so that class attributes can be set before __init_subclass__ is called. py_init_subclass_op = custom_op( - arg_types=[object_rprimitive], + arg_types=[object_rprimitive, object_rprimitive], return_type=bool_rprimitive, c_function_name="CPy_InitSubclass", error_kind=ERR_FALSE, From 1e0b867015f4b8a88d155edbf269e7b283836ff4 Mon Sep 17 00:00:00 2001 From: MarthalaJagruthiReddy <132251392+MarthalaJagruthiReddy@users.noreply.github.com> Date: Sat, 5 Sep 2026 10:42:53 -0700 Subject: [PATCH 4/5] Add mypyc class keyword regression tests Cover native and non-native class keyword forwarding and update the corresponding IR expectations. --- mypyc/test-data/irbuild-classes.test | 160 ++++++++++++++------------- mypyc/test-data/run-classes.test | 30 +++++ 2 files changed, 114 insertions(+), 76 deletions(-) diff --git a/mypyc/test-data/irbuild-classes.test b/mypyc/test-data/irbuild-classes.test index 8eaa63e4583b5..b831f55c21c7d 100644 --- a/mypyc/test-data/irbuild-classes.test +++ b/mypyc/test-data/irbuild-classes.test @@ -229,39 +229,42 @@ def __top_level__(): r35 :: str r36 :: i32 r37 :: bit - r38 :: bool - r39 :: object - r40 :: str - r41, r42 :: object - r43 :: str - r44 :: tuple - r45 :: i32 - r46 :: bit - r47 :: dict - r48 :: str - r49 :: i32 - r50 :: bit - r51 :: bool - r52, r53 :: object - r54 :: dict - r55 :: str - r56 :: object - r57 :: dict - r58 :: str - r59, r60 :: object - r61 :: tuple - r62 :: str - r63, r64 :: object - r65, r66 :: bool - r67, r68 :: str - r69 :: tuple - r70 :: i32 - r71 :: bit - r72 :: dict - r73 :: str - r74 :: i32 - r75 :: bit - r76 :: bool + r38 :: dict + r39 :: bool + r40 :: object + r41 :: str + r42, r43 :: object + r44 :: str + r45 :: tuple + r46 :: i32 + r47 :: bit + r48 :: dict + r49 :: str + r50 :: i32 + r51 :: bit + r52 :: dict + r53 :: bool + r54, r55 :: object + r56 :: dict + r57 :: str + r58 :: object + r59 :: dict + r60 :: str + r61, r62 :: object + r63 :: tuple + r64 :: str + r65, r66 :: object + r67, r68 :: bool + r69, r70 :: str + r71 :: tuple + r72 :: i32 + r73 :: bit + r74 :: dict + r75 :: str + r76 :: i32 + r77 :: bit + r78 :: dict + r79 :: bool L0: r0 = builtins :: module r1 = load_address _Py_NoneStruct @@ -309,47 +312,50 @@ L2: r35 = 'C' r36 = PyDict_SetItem(r34, r35, r27) r37 = r36 >= 0 :: signed - r38 = CPy_InitSubclass(r27) - r39 = :: object - r40 = '__main__' - r41 = __main__.S_template :: type - r42 = CPyType_FromTemplate(r41, r39, r40) - r43 = '__mypyc_attrs__' - r44 = CPyTuple_LoadEmptyTupleConstant() - r45 = PyObject_SetAttr(r42, r43, r44) - r46 = r45 >= 0 :: signed - __main__.S = r42 :: type - r47 = __main__.globals :: static - r48 = 'S' - r49 = PyDict_SetItem(r47, r48, r42) - r50 = r49 >= 0 :: signed - r51 = CPy_InitSubclass(r42) - r52 = __main__.C :: type - r53 = __main__.S :: type - r54 = __main__.globals :: static - r55 = 'Generic' - r56 = CPyDict_GetItem(r54, r55) - r57 = __main__.globals :: static - r58 = 'T' - r59 = CPyDict_GetItem(r57, r58) - r60 = PyObject_GetItem(r56, r59) - r61 = PyTuple_Pack(3, r52, r53, r60) - r62 = '__main__' - r63 = __main__.D_template :: type - r64 = CPyType_FromTemplate(r63, r61, r62) - r65 = D_trait_vtable_setup() - r66 = D_coroutine_setup(r64) - r67 = '__mypyc_attrs__' - r68 = '__dict__' - r69 = PyTuple_Pack(1, r68) - r70 = PyObject_SetAttr(r64, r67, r69) - r71 = r70 >= 0 :: signed - __main__.D = r64 :: type - r72 = __main__.globals :: static - r73 = 'D' - r74 = PyDict_SetItem(r72, r73, r64) - r75 = r74 >= 0 :: signed - r76 = CPy_InitSubclass(r64) + r38 = PyDict_New() + r39 = CPy_InitSubclass(r27, r38) + r40 = :: object + r41 = '__main__' + r42 = __main__.S_template :: type + r43 = CPyType_FromTemplate(r42, r40, r41) + r44 = '__mypyc_attrs__' + r45 = CPyTuple_LoadEmptyTupleConstant() + r46 = PyObject_SetAttr(r43, r44, r45) + r47 = r46 >= 0 :: signed + __main__.S = r43 :: type + r48 = __main__.globals :: static + r49 = 'S' + r50 = PyDict_SetItem(r48, r49, r43) + r51 = r50 >= 0 :: signed + r52 = PyDict_New() + r53 = CPy_InitSubclass(r43, r52) + r54 = __main__.C :: type + r55 = __main__.S :: type + r56 = __main__.globals :: static + r57 = 'Generic' + r58 = CPyDict_GetItem(r56, r57) + r59 = __main__.globals :: static + r60 = 'T' + r61 = CPyDict_GetItem(r59, r60) + r62 = PyObject_GetItem(r58, r61) + r63 = PyTuple_Pack(3, r54, r55, r62) + r64 = '__main__' + r65 = __main__.D_template :: type + r66 = CPyType_FromTemplate(r65, r63, r64) + r67 = D_trait_vtable_setup() + r68 = D_coroutine_setup(r66) + r69 = '__mypyc_attrs__' + r70 = '__dict__' + r71 = PyTuple_Pack(1, r70) + r72 = PyObject_SetAttr(r66, r69, r71) + r73 = r72 >= 0 :: signed + __main__.D = r66 :: type + r74 = __main__.globals :: static + r75 = 'D' + r76 = PyDict_SetItem(r74, r75, r66) + r77 = r76 >= 0 :: signed + r78 = PyDict_New() + r79 = CPy_InitSubclass(r66, r78) return 1 [case testIsInstance] @@ -3055,7 +3061,8 @@ def __top_level__(): r44 :: str r45 :: i32 r46 :: bit - r47 :: bool + r47 :: dict + r48 :: bool L0: r0 = builtins :: module r1 = load_address _Py_NoneStruct @@ -3110,7 +3117,8 @@ L2: r44 = 'B' r45 = PyObject_SetAttr(r34, r44, r43) r46 = r45 >= 0 :: signed - r47 = CPy_InitSubclass(r12) + r47 = PyDict_New() + r48 = CPy_InitSubclass(r12, r47) return 1 [case testClassVarSelfReferenceNonExt_withgil_toplevel] diff --git a/mypyc/test-data/run-classes.test b/mypyc/test-data/run-classes.test index 77f029b6b80b5..2527d234e21bb 100644 --- a/mypyc/test-data/run-classes.test +++ b/mypyc/test-data/run-classes.test @@ -1226,6 +1226,36 @@ assert GrandChild.required == 10, f"expected 10, got {GrandChild.required}" assert NoOverride.name == "base" assert NoOverride.required == 4, f"expected 4, got {NoOverride.required}" +[case testInitSubclassWithClassKeywords] +from mypy_extensions import mypyc_attr + +native_seen: dict[str, object] = {} + +class NativeBase: + def __init_subclass__(cls, **kwargs: object) -> None: + global native_seen + native_seen = kwargs + +class NativeChild(NativeBase, marker=1, label="native"): + pass + +non_ext_seen: dict[str, object] = {} + +@mypyc_attr(native_class=False) +class NonExtBase: + def __init_subclass__(cls, **kwargs: object) -> None: + global non_ext_seen + non_ext_seen = kwargs + +class NonExtChild(NonExtBase, marker=2, label="non-ext"): + pass + +def test_native_class_keywords() -> None: + assert native_seen == {"marker": 1, "label": "native"} + +def test_non_ext_class_keywords() -> None: + assert non_ext_seen == {"marker": 2, "label": "non-ext"} + [case testDefaultVars] from typing import Optional class A: From 74a6af97fc613edf27a2fda6976d556d877b1433 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 10 Sep 2026 16:06:24 -0400 Subject: [PATCH 5/5] Pass class keywords to metaclass __prepare__ --- mypyc/irbuild/classdef.py | 27 +++++++++++++++++++++++---- mypyc/test-data/run-classes.test | 30 ++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 4 deletions(-) diff --git a/mypyc/irbuild/classdef.py b/mypyc/irbuild/classdef.py index 1c072bc77c21e..1070077914443 100644 --- a/mypyc/irbuild/classdef.py +++ b/mypyc/irbuild/classdef.py @@ -271,7 +271,7 @@ def create_non_ext_info(self) -> NonExtClassInfo: # Class header expressions are evaluated before invoking __prepare__. self.class_keyword_values = load_class_keyword_values(self.builder, self.cdef) non_ext_dict = setup_non_ext_dict( - self.builder, self.cdef, non_ext_metaclass, non_ext_bases + self.builder, self.cdef, non_ext_metaclass, non_ext_bases, self.class_keyword_values ) # We populate __annotations__ for non-extension classes # because dataclasses uses it to determine which attributes to compute on. @@ -647,11 +647,17 @@ def find_non_ext_metaclass(builder: IRBuilder, cdef: ClassDef, bases: Value) -> def setup_non_ext_dict( - builder: IRBuilder, cdef: ClassDef, metaclass: Value, bases: Value + builder: IRBuilder, + cdef: ClassDef, + metaclass: Value, + bases: Value, + class_keyword_values: list[tuple[str, Value]], ) -> Value: """Initialize the class dictionary for a non-extension class. - This class dictionary is passed to the metaclass constructor. + This class dictionary is passed to the metaclass constructor. If the + metaclass defines ``__prepare__``, class definition keywords are passed to + it as well. """ # Check if the metaclass defines a __prepare__ method, and if so, call it. has_prepare = builder.primitive_op( @@ -666,7 +672,20 @@ def setup_non_ext_dict( builder.activate_block(true_block) cls_name = builder.load_str(cdef.name) prepare_meth = builder.py_get_attr(metaclass, "__prepare__", cdef.line) - prepare_dict = builder.py_call(prepare_meth, [cls_name, bases], cdef.line) + prepare_args = [cls_name, bases] + prepare_arg_kinds = [ARG_POS, ARG_POS] + prepare_arg_names: list[str | None] = [None, None] + for name, value in class_keyword_values: + prepare_args.append(value) + prepare_arg_kinds.append(ARG_NAMED) + prepare_arg_names.append(name) + prepare_dict = builder.py_call( + prepare_meth, + prepare_args, + cdef.line, + arg_kinds=prepare_arg_kinds, + arg_names=prepare_arg_names, + ) builder.assign(non_ext_dict, prepare_dict, cdef.line) builder.goto(exit_block) diff --git a/mypyc/test-data/run-classes.test b/mypyc/test-data/run-classes.test index 2527d234e21bb..0d6a50848ac9e 100644 --- a/mypyc/test-data/run-classes.test +++ b/mypyc/test-data/run-classes.test @@ -1256,6 +1256,36 @@ def test_native_class_keywords() -> None: def test_non_ext_class_keywords() -> None: assert non_ext_seen == {"marker": 2, "label": "non-ext"} +[case testPrepareReceivedClassKeywords] +from typing import Any +from mypy_extensions import mypyc_attr + +seen: dict[str, object] = {} + +@mypyc_attr(native_class=False) +class Meta(type): + @classmethod + def __prepare__( + mcls, name: str, bases: tuple[type, ...], /, **kwargs: Any + ) -> dict[str, object]: + global seen + seen = kwargs + return {} + +@mypyc_attr(native_class=False) +class Base(metaclass=Meta): + def __init_subclass__(cls, **kwargs: object) -> None: + pass + +@mypyc_attr(native_class=False) +class Child(Base, marker=1): + pass + +def test_prepare_received_class_keywords() -> None: + assert seen == {"marker": 1}, seen + +test_prepare_received_class_keywords() + [case testDefaultVars] from typing import Optional class A: