Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 39 additions & 4 deletions mypyc/irbuild/classdef.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from typing import Final

from mypy.nodes import (
ARG_NAMED,
ARG_POS,
EXCLUDED_ENUM_ATTRIBUTES,
TYPE_VAR_TUPLE_KIND,
Expand Down Expand Up @@ -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
)
Comment on lines +271 to 275

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setup_non_ext_dict calls __prepare__ of the metaclass which it looks like should also receive the keyword args.

This test works with interpreted python and fails when compiled:

from typing import Any, MutableMapping
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
    ) -> MutableMapping[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()

Expand All @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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.

Expand Down
2 changes: 1 addition & 1 deletion mypyc/lib-rt/CPy.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
4 changes: 2 additions & 2 deletions mypyc/lib-rt/misc_ops.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion mypyc/primitives/misc_ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
160 changes: 84 additions & 76 deletions mypyc/test-data/irbuild-classes.test
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -309,47 +312,50 @@ L2:
r35 = 'C'
r36 = PyDict_SetItem(r34, r35, r27)
r37 = r36 >= 0 :: signed
r38 = CPy_InitSubclass(r27)
r39 = <error> :: 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 = <error> :: 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]
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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]
Expand Down
30 changes: 30 additions & 0 deletions mypyc/test-data/run-classes.test
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Loading