diff --git a/mypyc/analysis/attrdefined.py b/mypyc/analysis/attrdefined.py index 4fd0017257a0..5be57d767e35 100644 --- a/mypyc/analysis/attrdefined.py +++ b/mypyc/analysis/attrdefined.py @@ -138,6 +138,7 @@ def analyze_always_defined_attrs_in_class(cl: ClassIR, seen: set[ClassIR]) -> No or cl.builtin_base is not None or cl.children is None or cl.is_serializable() + or cl.has_method("__new__") ): # Give up -- we can't enforce that attributes are always defined. return diff --git a/mypyc/codegen/emitclass.py b/mypyc/codegen/emitclass.py index ecf8c37f83c9..0931c849131d 100644 --- a/mypyc/codegen/emitclass.py +++ b/mypyc/codegen/emitclass.py @@ -5,6 +5,7 @@ from collections.abc import Mapping from typing import Callable +from mypy.nodes import ARG_STAR, ARG_STAR2 from mypyc.codegen.cstring import c_string_initializer from mypyc.codegen.emit import Emitter, HeaderDeclaration, ReturnHandler from mypyc.codegen.emitfunc import native_function_doc_initializer, native_function_header @@ -224,7 +225,7 @@ def generate_class(cl: ClassIR, module: str, emitter: Emitter) -> None: name = cl.name name_prefix = cl.name_prefix(emitter.names) - setup_name = f"{name_prefix}_setup" + setup_name = emitter.native_function_name(cl.setup) new_name = f"{name_prefix}_new" finalize_name = f"{name_prefix}_finalize" members_name = f"{name_prefix}_members" @@ -317,10 +318,8 @@ def emit_line() -> None: fields["tp_basicsize"] = base_size if generate_full: - # Declare setup method that allocates and initializes an object. type is the - # type of the class being initialized, which could be another class if there - # is an interpreted subclass. - emitter.emit_line(f"static PyObject *{setup_name}(PyTypeObject *type);") + assert cl.setup is not None + emitter.emit_line(native_function_header(cl.setup, emitter) + ";") assert cl.ctor is not None emitter.emit_line(native_function_header(cl.ctor, emitter) + ";") @@ -390,9 +389,7 @@ def emit_line() -> None: emitter.emit_line() if generate_full: - generate_setup_for_class( - cl, setup_name, defaults_fn, vtable_name, shadow_vtable_name, emitter - ) + generate_setup_for_class(cl, defaults_fn, vtable_name, shadow_vtable_name, emitter) emitter.emit_line() generate_constructor_for_class(cl, cl.ctor, init_fn, setup_name, vtable_name, emitter) emitter.emit_line() @@ -579,16 +576,16 @@ def generate_vtable( def generate_setup_for_class( cl: ClassIR, - func_name: str, defaults_fn: FuncIR | None, vtable_name: str, shadow_vtable_name: str | None, emitter: Emitter, ) -> None: """Generate a native function that allocates an instance of a class.""" - emitter.emit_line("static PyObject *") - emitter.emit_line(f"{func_name}(PyTypeObject *type)") + emitter.emit_line(native_function_header(cl.setup, emitter)) emitter.emit_line("{") + type_arg_name = REG_PREFIX + cl.setup.sig.args[0].name + emitter.emit_line(f"PyTypeObject *type = (PyTypeObject*){type_arg_name};") struct_name = cl.struct_name(emitter.names) emitter.emit_line(f"{struct_name} *self;") @@ -663,6 +660,35 @@ def emit_attr_defaults_func_call(defaults_fn: FuncIR, self_name: str, emitter: E ) +def emit_setup_or_dunder_new_call( + cl: ClassIR, + setup_name: str, + type_arg: str, + native_prefix: bool, + new_args: str, + emitter: Emitter, +) -> None: + def emit_null_check() -> None: + emitter.emit_line("if (self == NULL)") + emitter.emit_line(" return NULL;") + + new_fn = cl.get_method("__new__") + if not new_fn: + emitter.emit_line(f"PyObject *self = {setup_name}({type_arg});") + emit_null_check() + return + prefix = emitter.get_group_prefix(new_fn.decl) + NATIVE_PREFIX if native_prefix else PREFIX + all_args = type_arg + if new_args != "": + all_args += ", " + new_args + emitter.emit_line(f"PyObject *self = {prefix}{new_fn.cname(emitter.names)}({all_args});") + emit_null_check() + + # skip __init__ if __new__ returns some other type + emitter.emit_line(f"if (Py_TYPE(self) != {emitter.type_struct_name(cl)})") + emitter.emit_line(" return self;") + + def generate_constructor_for_class( cl: ClassIR, fn: FuncDecl, @@ -674,17 +700,30 @@ def generate_constructor_for_class( """Generate a native function that allocates and initializes an instance of a class.""" emitter.emit_line(f"{native_function_header(fn, emitter)}") emitter.emit_line("{") - emitter.emit_line(f"PyObject *self = {setup_name}({emitter.type_struct_name(cl)});") - emitter.emit_line("if (self == NULL)") - emitter.emit_line(" return NULL;") - args = ", ".join(["self"] + [REG_PREFIX + arg.name for arg in fn.sig.args]) + + fn_args = [REG_PREFIX + arg.name for arg in fn.sig.args] + type_arg = "(PyObject *)" + emitter.type_struct_name(cl) + new_args = ", ".join(fn_args) + + use_wrapper = ( + cl.has_method("__new__") + and len(fn.sig.args) == 2 + and fn.sig.args[0].kind == ARG_STAR + and fn.sig.args[1].kind == ARG_STAR2 + ) + emit_setup_or_dunder_new_call(cl, setup_name, type_arg, not use_wrapper, new_args, emitter) + + args = ", ".join(["self"] + fn_args) if init_fn is not None: + prefix = PREFIX if use_wrapper else NATIVE_PREFIX + cast = "!= NULL ? 0 : -1" if use_wrapper else "" emitter.emit_line( - "char res = {}{}{}({});".format( + "char res = {}{}{}({}){};".format( emitter.get_group_prefix(init_fn.decl), - NATIVE_PREFIX, + prefix, init_fn.cname(emitter.names), args, + cast, ) ) emitter.emit_line("if (res == 2) {") @@ -717,7 +756,7 @@ def generate_init_for_class(cl: ClassIR, init_fn: FuncIR, emitter: Emitter) -> s emitter.emit_line("static int") emitter.emit_line(f"{func_name}(PyObject *self, PyObject *args, PyObject *kwds)") emitter.emit_line("{") - if cl.allow_interpreted_subclasses or cl.builtin_base: + if cl.allow_interpreted_subclasses or cl.builtin_base or cl.has_method("__new__"): emitter.emit_line( "return {}{}(self, args, kwds) != NULL ? 0 : -1;".format( PREFIX, init_fn.cname(emitter.names) @@ -750,15 +789,22 @@ def generate_new_for_class( emitter.emit_line("return NULL;") emitter.emit_line("}") - if not init_fn or cl.allow_interpreted_subclasses or cl.builtin_base or cl.is_serializable(): + type_arg = "(PyObject*)type" + new_args = "args, kwds" + emit_setup_or_dunder_new_call(cl, setup_name, type_arg, False, new_args, emitter) + if ( + not init_fn + or cl.allow_interpreted_subclasses + or cl.builtin_base + or cl.is_serializable() + or cl.has_method("__new__") + ): # Match Python semantics -- __new__ doesn't call __init__. - emitter.emit_line(f"return {setup_name}(type);") + emitter.emit_line("return self;") else: # __new__ of a native class implicitly calls __init__ so that we # can enforce that instances are always properly initialized. This # is needed to support always defined attributes. - emitter.emit_line(f"PyObject *self = {setup_name}(type);") - emitter.emit_lines("if (self == NULL)", " return NULL;") emitter.emit_line( f"PyObject *ret = {PREFIX}{init_fn.cname(emitter.names)}(self, args, kwds);" ) diff --git a/mypyc/codegen/emitmodule.py b/mypyc/codegen/emitmodule.py index e31fcf8ea0c9..ca5db52ab7da 100644 --- a/mypyc/codegen/emitmodule.py +++ b/mypyc/codegen/emitmodule.py @@ -1274,8 +1274,8 @@ def is_fastcall_supported(fn: FuncIR, capi_version: tuple[int, int]) -> bool: if fn.name == "__call__": # We can use vectorcalls (PEP 590) when supported return True - # TODO: Support fastcall for __init__. - return fn.name != "__init__" + # TODO: Support fastcall for __init__ and __new__. + return fn.name != "__init__" and fn.name != "__new__" return True diff --git a/mypyc/codegen/emitwrapper.py b/mypyc/codegen/emitwrapper.py index cd1684255855..2e5d7efa4e98 100644 --- a/mypyc/codegen/emitwrapper.py +++ b/mypyc/codegen/emitwrapper.py @@ -238,7 +238,7 @@ def generate_legacy_wrapper_function( real_args = list(fn.args) if fn.sig.num_bitmap_args: real_args = real_args[: -fn.sig.num_bitmap_args] - if fn.class_name and fn.decl.kind != FUNC_STATICMETHOD: + if fn.class_name and (fn.decl.name == "__new__" or fn.decl.kind != FUNC_STATICMETHOD): arg = real_args.pop(0) emitter.emit_line(f"PyObject *obj_{arg.name} = self;") diff --git a/mypyc/ir/class_ir.py b/mypyc/ir/class_ir.py index f6015b64dcdd..0a56aaf5d101 100644 --- a/mypyc/ir/class_ir.py +++ b/mypyc/ir/class_ir.py @@ -5,9 +5,9 @@ from typing import NamedTuple from mypyc.common import PROPSET_PREFIX, JsonDict -from mypyc.ir.func_ir import FuncDecl, FuncIR, FuncSignature +from mypyc.ir.func_ir import FuncDecl, FuncIR, FuncSignature, RuntimeArg from mypyc.ir.ops import DeserMaps, Value -from mypyc.ir.rtypes import RInstance, RType, deserialize_type +from mypyc.ir.rtypes import RInstance, RType, deserialize_type, object_rprimitive from mypyc.namegen import NameGenerator, exported_name # Some notes on the vtable layout: Each concrete class has a vtable @@ -133,6 +133,16 @@ def __init__( self.builtin_base: str | None = None # Default empty constructor self.ctor = FuncDecl(name, None, module_name, FuncSignature([], RInstance(self))) + # Declare setup method that allocates and initializes an object. type is the + # type of the class being initialized, which could be another class if there + # is an interpreted subclass. + # TODO: Make it a regular method and generate its body in IR + self.setup = FuncDecl( + "__mypyc__" + name + "_setup", + None, + module_name, + FuncSignature([RuntimeArg("type", object_rprimitive)], RInstance(self)), + ) # Attributes defined in the class (not inherited) self.attributes: dict[str, RType] = {} # Deletable attributes diff --git a/mypyc/irbuild/expression.py b/mypyc/irbuild/expression.py index e82203021ae3..4409b1acff26 100644 --- a/mypyc/irbuild/expression.py +++ b/mypyc/irbuild/expression.py @@ -57,6 +57,7 @@ from mypyc.ir.ops import ( Assign, BasicBlock, + Call, ComparisonOp, Integer, LoadAddress, @@ -472,23 +473,42 @@ def translate_super_method_call(builder: IRBuilder, expr: CallExpr, callee: Supe if callee.name in base.method_decls: break else: - if ( - ir.is_ext_class - and ir.builtin_base is None - and not ir.inherits_python - and callee.name == "__init__" - and len(expr.args) == 0 - ): - # Call translates to object.__init__(self), which is a - # no-op, so omit the call. - return builder.none() + if ir.is_ext_class and ir.builtin_base is None and not ir.inherits_python: + if callee.name == "__init__" and len(expr.args) == 0: + # Call translates to object.__init__(self), which is a + # no-op, so omit the call. + return builder.none() + elif callee.name == "__new__": + # object.__new__(cls) + assert ( + len(expr.args) == 1 + ), f"Expected object.__new__() call to have exactly 1 argument, got {len(expr.args)}" + typ_arg = expr.args[0] + method_args = builder.fn_info.fitem.arg_names + if ( + isinstance(typ_arg, NameExpr) + and len(method_args) > 0 + and method_args[0] == typ_arg.name + ): + subtype = builder.accept(expr.args[0]) + return builder.add(Call(ir.setup, [subtype], expr.line)) + + if callee.name == "__new__": + call = "super().__new__()" + if not ir.is_ext_class: + builder.error(f"{call} not supported for non-extension classes", expr.line) + if ir.inherits_python: + builder.error( + f"{call} not supported for classes inheriting from non-native classes", + expr.line, + ) return translate_call(builder, expr, callee) decl = base.method_decl(callee.name) arg_values = [builder.accept(arg) for arg in expr.args] arg_kinds, arg_names = expr.arg_kinds.copy(), expr.arg_names.copy() - if decl.kind != FUNC_STATICMETHOD: + if decl.kind != FUNC_STATICMETHOD and decl.name != "__new__": # Grab first argument vself: Value = builder.self() if decl.kind == FUNC_CLASSMETHOD: diff --git a/mypyc/irbuild/prepare.py b/mypyc/irbuild/prepare.py index 83ec3f7c1d38..95c8c448d642 100644 --- a/mypyc/irbuild/prepare.py +++ b/mypyc/irbuild/prepare.py @@ -193,9 +193,9 @@ def prepare_func_def( create_generator_class_if_needed(module_name, class_name, fdef, mapper) kind = ( - FUNC_STATICMETHOD - if fdef.is_static - else (FUNC_CLASSMETHOD if fdef.is_class else FUNC_NORMAL) + FUNC_CLASSMETHOD + if fdef.is_class + else (FUNC_STATICMETHOD if fdef.is_static else FUNC_NORMAL) ) sig = mapper.fdef_to_sig(fdef, options.strict_dunders_typing) decl = FuncDecl(fdef.name, class_name, module_name, sig, kind) @@ -555,21 +555,57 @@ def add_setter_declaration( ir.method_decls[setter_name] = decl +def check_matching_args(init_sig: FuncSignature, new_sig: FuncSignature) -> bool: + num_init_args = len(init_sig.args) - init_sig.num_bitmap_args + num_new_args = len(new_sig.args) - new_sig.num_bitmap_args + if num_init_args != num_new_args: + return False + + for idx in range(1, num_init_args): + init_arg = init_sig.args[idx] + new_arg = new_sig.args[idx] + if init_arg.type != new_arg.type: + return False + + if init_arg.kind != new_arg.kind: + return False + + return True + + def prepare_init_method(cdef: ClassDef, ir: ClassIR, module_name: str, mapper: Mapper) -> None: # Set up a constructor decl init_node = cdef.info["__init__"].node + + new_node: SymbolNode | None = None + new_symbol = cdef.info.get("__new__") + # We are only interested in __new__ method defined in a user-defined class, + # so we ignore it if it comes from a builtin type. It's usually builtins.object + # but could also be builtins.type for metaclasses so we detect the prefix which + # matches both. + if new_symbol and new_symbol.fullname and not new_symbol.fullname.startswith("builtins."): + new_node = new_symbol.node + if isinstance(new_node, (Decorator, OverloadedFuncDef)): + new_node = get_func_def(new_node) if not ir.is_trait and not ir.builtin_base and isinstance(init_node, FuncDef): init_sig = mapper.fdef_to_sig(init_node, True) + args_match = True + if isinstance(new_node, FuncDef): + new_sig = mapper.fdef_to_sig(new_node, True) + args_match = check_matching_args(init_sig, new_sig) defining_ir = mapper.type_to_ir.get(init_node.info) # If there is a nontrivial __init__ that wasn't defined in an # extension class, we need to make the constructor take *args, # **kwargs so it can call tp_init. if ( - defining_ir is None - or not defining_ir.is_ext_class - or cdef.info["__init__"].plugin_generated - ) and init_node.info.fullname != "builtins.object": + ( + defining_ir is None + or not defining_ir.is_ext_class + or cdef.info["__init__"].plugin_generated + ) + and init_node.info.fullname != "builtins.object" + ) or not args_match: init_sig = FuncSignature( [ init_sig.args[0], diff --git a/mypyc/lib-rt/misc_ops.c b/mypyc/lib-rt/misc_ops.c index b7593491a6e6..ca09c347b4ff 100644 --- a/mypyc/lib-rt/misc_ops.c +++ b/mypyc/lib-rt/misc_ops.c @@ -227,6 +227,17 @@ PyObject *CPyType_FromTemplate(PyObject *template, if (!name) goto error; + if (template_->tp_doc) { + // cpython expects tp_doc to be heap-allocated so convert it here to + // avoid segfaults on deallocation. + Py_ssize_t size = strlen(template_->tp_doc) + 1; + char *doc = (char *)PyMem_Malloc(size); + if (!doc) + goto error; + memcpy(doc, template_->tp_doc, size); + template_->tp_doc = doc; + } + // Allocate the type and then copy the main stuff in. t = (PyHeapTypeObject*)PyType_GenericAlloc(&PyType_Type, 0); if (!t) diff --git a/mypyc/test-data/fixtures/ir.py b/mypyc/test-data/fixtures/ir.py index c041c661741c..fb5512b77279 100644 --- a/mypyc/test-data/fixtures/ir.py +++ b/mypyc/test-data/fixtures/ir.py @@ -3,7 +3,7 @@ import _typeshed from typing import ( - TypeVar, Generic, List, Iterator, Iterable, Dict, Optional, Tuple, Any, Set, + Self, TypeVar, Generic, List, Iterator, Iterable, Dict, Optional, Tuple, Any, Set, overload, Mapping, Union, Callable, Sequence, FrozenSet, Protocol ) @@ -40,9 +40,11 @@ def __pow__(self, other: T_contra, modulo: _M) -> T_co: ... class object: __class__: type + def __new__(cls) -> Self: pass def __init__(self) -> None: pass def __eq__(self, x: object) -> bool: pass def __ne__(self, x: object) -> bool: pass + def __str__(self) -> str: pass class type: def __init__(self, o: object) -> None: ... diff --git a/mypyc/test-data/fixtures/typing-full.pyi b/mypyc/test-data/fixtures/typing-full.pyi index d37129bc2e0b..8d89e4f93bc9 100644 --- a/mypyc/test-data/fixtures/typing-full.pyi +++ b/mypyc/test-data/fixtures/typing-full.pyi @@ -32,6 +32,7 @@ Final = 0 TypedDict = 0 NoReturn = 0 NewType = 0 +Self = 0 Callable: _SpecialForm Union: _SpecialForm Literal: _SpecialForm diff --git a/mypyc/test-data/irbuild-classes.test b/mypyc/test-data/irbuild-classes.test index 68bc18c7bdeb..3624e10a46a2 100644 --- a/mypyc/test-data/irbuild-classes.test +++ b/mypyc/test-data/irbuild-classes.test @@ -1607,3 +1607,78 @@ def native_class(x): L0: r0 = CPy_TYPE(x) return r0 + +[case testDunderNew] +from __future__ import annotations + +class Test: + val: int + + def __new__(cls, val: int) -> Test: + obj = super().__new__(cls) + obj.val = val + return obj + +def fn() -> Test: + return Test.__new__(Test, 42) + +class NewClassMethod: + val: int + + @classmethod + def __new__(cls, val: int) -> NewClassMethod: + obj = super().__new__(cls) + obj.val = val + return obj + +def fn2() -> NewClassMethod: + return NewClassMethod.__new__(42) + +[out] +def Test.__new__(cls, val): + cls :: object + val :: int + r0, obj :: __main__.Test + r1 :: bool +L0: + r0 = __mypyc__Test_setup(cls) + obj = r0 + obj.val = val; r1 = is_error + return obj +def fn(): + r0 :: object + r1 :: __main__.Test +L0: + r0 = __main__.Test :: type + r1 = Test.__new__(r0, 84) + return r1 +def NewClassMethod.__new__(cls, val): + cls :: object + val :: int + r0, obj :: __main__.NewClassMethod + r1 :: bool +L0: + r0 = __mypyc__NewClassMethod_setup(cls) + obj = r0 + obj.val = val; r1 = is_error + return obj +def fn2(): + r0 :: object + r1 :: __main__.NewClassMethod +L0: + r0 = __main__.NewClassMethod :: type + r1 = NewClassMethod.__new__(r0, 84) + return r1 + +[case testUnsupportedDunderNew] +from __future__ import annotations +from mypy_extensions import mypyc_attr + +@mypyc_attr(native_class=False) +class NonNative: + def __new__(cls) -> NonNative: + return super().__new__(cls) # E: super().__new__() not supported for non-extension classes + +class InheritsPython(dict): + def __new__(cls) -> InheritsPython: + return super().__new__(cls) # E: super().__new__() not supported for classes inheriting from non-native classes diff --git a/mypyc/test-data/run-classes.test b/mypyc/test-data/run-classes.test index 6f1217bd36e6..7abe4b57fb81 100644 --- a/mypyc/test-data/run-classes.test +++ b/mypyc/test-data/run-classes.test @@ -3277,3 +3277,432 @@ def test_python_class() -> None: [file dynamic.py] class Dyn: pass + +[case testDunderNew] +from __future__ import annotations +from typing import Any, Union + +from testutil import assertRaises + +class Add: + l: IntLike + r: IntLike + + def __new__(cls, l: IntLike, r: IntLike) -> Any: + return ( + l if r == 0 else + r if l == 0 else + super().__new__(cls) + ) + + def __init__(self, l: IntLike, r: IntLike): + self.l = l + self.r = r + +IntLike = Union[int, Add] + +class RaisesException: + def __new__(cls, val: int) -> RaisesException: + if val == 0: + raise RuntimeError("Invalid value!") + return super().__new__(cls) + + def __init__(self, val: int) -> None: + self.val = val + +class ClsArgNotPassed: + def __new__(cls) -> Any: + return super().__new__(str) + +def test_dunder_new() -> None: + add_instance: Any = Add(1, 5) + assert type(add_instance) == Add + assert add_instance.l == 1 + assert add_instance.r == 5 + + # TODO: explicit types should not be needed but mypy does not use + # the return type of __new__ which makes mypyc add casts to Add. + right_int: Any = Add(0, 5) + assert type(right_int) == int + assert right_int == 5 + + left_int: Any = Add(1, 0) + assert type(left_int) == int + assert left_int == 1 + + with assertRaises(RuntimeError, "Invalid value!"): + raised = RaisesException(0) + + not_raised = RaisesException(1) + assert not_raised.val == 1 + + with assertRaises(TypeError, "object.__new__(str) is not safe, use str.__new__()"): + str_as_cls = ClsArgNotPassed() + + +[case testDunderNewInInterpreted] +from __future__ import annotations +from typing import Any, Union + +class Add: + l: IntLike + r: IntLike + + def __new__(cls, l: IntLike, r: IntLike) -> Any: + print(f'running __new__ with {l} and {r}') + + return ( + l if r == 0 else + r if l == 0 else + super().__new__(cls) + ) + + def __init__(self, l: IntLike, r: IntLike): + self.l = l + self.r = r + + def __repr__(self) -> str: + return f'({self.l} + {self.r})' + +IntLike = Union[int, Add] + +class RaisesException: + def __new__(cls, val: int) -> RaisesException: + if val == 0: + raise RuntimeError("Invalid value!") + return super().__new__(cls) + + def __init__(self, val: int) -> None: + self.val = val + +class ClsArgNotPassed: + def __new__(cls) -> Any: + return super().__new__(str) + +[file driver.py] +from native import Add, ClsArgNotPassed, RaisesException + +from testutil import assertRaises + +print(f'{Add(1, 5)=}') +print(f'{Add(0, 5)=}') +print(f'{Add(1, 0)=}') + +with assertRaises(RuntimeError, "Invalid value!"): + raised = RaisesException(0) + +not_raised = RaisesException(1) +assert not_raised.val == 1 + +with assertRaises(TypeError, "object.__new__(str) is not safe, use str.__new__()"): + str_as_cls = ClsArgNotPassed() + +[out] +running __new__ with 1 and 5 +Add(1, 5)=(1 + 5) +running __new__ with 0 and 5 +Add(0, 5)=5 +running __new__ with 1 and 0 +Add(1, 0)=1 + +[case testInheritedDunderNew] +from __future__ import annotations +from mypy_extensions import mypyc_attr +from typing_extensions import Self + +from m import interpreted_subclass + +@mypyc_attr(allow_interpreted_subclasses=True) +class Base: + val: int + + def __new__(cls, val: int) -> Self: + obj = super().__new__(cls) + obj.val = val + 1 + return obj + + def __init__(self, val: int) -> None: + self.init_val = val + +class Sub(Base): + def __new__(cls, val: int) -> Self: + return super().__new__(cls, val + 1) + + def __init__(self, val: int) -> None: + super().__init__(val) + self.init_val = self.init_val * 2 + +class SubWithoutNew(Base): + def __init__(self, val: int) -> None: + super().__init__(val) + self.init_val = self.init_val * 2 + +class BaseWithoutInterpretedSubclasses: + val: int + + def __new__(cls, val: int) -> Self: + obj = super().__new__(cls) + obj.val = val + 1 + return obj + + def __init__(self, val: int) -> None: + self.init_val = val + +class SubNoInterpreted(BaseWithoutInterpretedSubclasses): + def __new__(cls, val: int) -> Self: + return super().__new__(cls, val + 1) + + def __init__(self, val: int) -> None: + super().__init__(val) + self.init_val = self.init_val * 2 + +class SubNoInterpretedWithoutNew(BaseWithoutInterpretedSubclasses): + def __init__(self, val: int) -> None: + super().__init__(val) + self.init_val = self.init_val * 2 + +def test_inherited_dunder_new() -> None: + b = Base(42) + assert type(b) == Base + assert b.val == 43 + assert b.init_val == 42 + + s = Sub(42) + assert type(s) == Sub + assert s.val == 44 + assert s.init_val == 84 + + s2 = SubWithoutNew(42) + assert type(s2) == SubWithoutNew + assert s2.val == 43 + assert s2.init_val == 84 + +def test_inherited_dunder_new_without_interpreted_subclasses() -> None: + b = BaseWithoutInterpretedSubclasses(42) + assert type(b) == BaseWithoutInterpretedSubclasses + assert b.val == 43 + assert b.init_val == 42 + + s = SubNoInterpreted(42) + assert type(s) == SubNoInterpreted + assert s.val == 44 + assert s.init_val == 84 + + s2 = SubNoInterpretedWithoutNew(42) + assert type(s2) == SubNoInterpretedWithoutNew + assert s2.val == 43 + assert s2.init_val == 84 + +def test_interpreted_subclass() -> None: + interpreted_subclass(Base) + +[file m.py] +from __future__ import annotations +from typing_extensions import Self + +def interpreted_subclass(base) -> None: + b = base(42) + assert type(b) == base + assert b.val == 43 + assert b.init_val == 42 + + class InterpretedSub(base): + def __new__(cls, val: int) -> Self: + return super().__new__(cls, val + 1) + + def __init__(self, val: int) -> None: + super().__init__(val) + self.init_val : int = self.init_val * 2 + + s = InterpretedSub(42) + assert type(s) == InterpretedSub + assert s.val == 44 + assert s.init_val == 84 + + class InterpretedSubWithoutNew(base): + def __init__(self, val: int) -> None: + super().__init__(val) + self.init_val : int = self.init_val * 2 + + s2 = InterpretedSubWithoutNew(42) + assert type(s2) == InterpretedSubWithoutNew + assert s2.val == 43 + assert s2.init_val == 84 + +[typing fixtures/typing-full.pyi] + +[case testDunderNewInitArgMismatch] +from __future__ import annotations +from testutil import assertRaises + +class Test0: + @classmethod + def __new__(cls, val: int = 42) -> Test0: + obj = super().__new__(cls) + obj.val = val + return obj + + def __init__(self) -> None: + self.val = 0 + +class Test1: + def __new__(cls, val: int) -> Test1: + obj = super().__new__(cls) + obj.val = val + return obj + + def __init__(self) -> None: + self.val = 0 + +class Test2: + def __new__(cls) -> Test2: + obj = super().__new__(cls) + return obj + + def __init__(self, val: int) -> None: + self.val = val + +def test_arg_mismatch() -> None: + t0 = Test0() + assert t0.val == 0 + t0 = Test0.__new__(1) + assert t0.val == 1 + with assertRaises(TypeError, "__new__() missing required argument 'val'"): + t1 = Test1() + t1 = Test1.__new__(Test1, 2) + assert t1.val == 2 + with assertRaises(TypeError, "__new__() takes at most 0 arguments"): + t2 = Test2(42) + t2 = Test2.__new__(Test2) + with assertRaises(AttributeError, "attribute 'val' of 'Test2' undefined"): + print(t2.val) + +[case testDunderNewInitArgMismatchInInterpreted] +from __future__ import annotations + +class Test0: + # TODO: It should be possible to annotate '@classmethod' here + # but when it's added calling __new__ in interpreted code + # without the explicit type param results in a TypeError. + def __new__(cls, val: int = 42) -> Test0: + obj = super().__new__(cls) + obj.val = val + return obj + + def __init__(self) -> None: + self.val = 0 + +class Test1: + def __new__(cls, val: int) -> Test1: + obj = super().__new__(cls) + obj.val = val + return obj + + def __init__(self) -> None: + self.val = 0 + +class Test2: + def __new__(cls) -> Test2: + obj = super().__new__(cls) + return obj + + def __init__(self, val: int) -> None: + self.val = val + +[file driver.py] +from native import Test0, Test1, Test2 +from testutil import assertRaises + +t0 = Test0() +assert t0.val == 0 +t0 = Test0.__new__(Test0, 1) +assert t0.val == 1 +with assertRaises(TypeError, "__new__() missing required argument 'val'"): + t1 = Test1() +t1 = Test1.__new__(Test1, 2) +assert t1.val == 2 +with assertRaises(TypeError, "__new__() takes at most 0 arguments"): + t2 = Test2(42) +t2 = Test2.__new__(Test2) +with assertRaises(AttributeError, "attribute 'val' of 'Test2' undefined"): + print(t2.val) + +[case testDunderNewAttributeAccess] +from __future__ import annotations + +from mypy_extensions import u8 +from testutil import assertRaises + +class Test: + native: int + generic: object + bitfield: u8 + default: int = 5 + + def __new__(cls, native: int, generic: object, bitfield: u8) -> Test: + obj = super().__new__(cls) + + with assertRaises(AttributeError, "attribute 'native' of 'Test' undefined"): + print(obj.native) + with assertRaises(AttributeError, "attribute 'generic' of 'Test' undefined"): + print(obj.generic) + with assertRaises(AttributeError, "attribute 'bitfield' of 'Test' undefined"): + print(obj.bitfield) + + obj.native = native + obj.generic = generic + obj.bitfield = bitfield + + obj.native = obj.native + 1 + obj.generic = obj.generic.__str__() + obj.bitfield = obj.bitfield & 0x0F + obj.default = obj.default * 2 + return obj + +def test_attribute_access() -> None: + t = Test(42, {}, 0xCC) + assert t.native == 43 + assert t.generic == "{}" + assert t.bitfield == 0x0C + assert t.default == 10 + +[case testDunderNewAttributeAccessInInterpreted] +from __future__ import annotations + +from mypy_extensions import u8 +from testutil import assertRaises + +class Test: + native: int + generic: object + bitfield: u8 + default: int = 5 + + def __new__(cls, native: int, generic: object, bitfield: u8) -> Test: + obj = super().__new__(cls) + + with assertRaises(AttributeError, "attribute 'native' of 'Test' undefined"): + print(obj.native) + with assertRaises(AttributeError, "attribute 'generic' of 'Test' undefined"): + print(obj.generic) + with assertRaises(AttributeError, "attribute 'bitfield' of 'Test' undefined"): + print(obj.bitfield) + + obj.native = native + obj.generic = generic + obj.bitfield = bitfield + + obj.native = obj.native + 1 + obj.generic = obj.generic.__str__() + obj.bitfield = obj.bitfield & 0x0F + obj.default = obj.default * 2 + return obj + +[file driver.py] +from native import Test + +t = Test(42, {}, 0xCC) +assert t.native == 43 +assert t.generic == "{}" +assert t.bitfield == 0x0C +assert t.default == 10