Skip to content

Commit

Permalink
Issue #610: Fix regression for code using __init__
Browse files Browse the repository at this point in the history
As mentioned in the changelog update a number of popular
libraries use ``__new__`` and ``__init__`` in their code
and the 10.3 change w.r.t. not calling ``__init__`` broke
those libraries.
  • Loading branch information
ronaldoussoren committed Jun 8, 2024
1 parent c9b9ac9 commit d9280a2
Show file tree
Hide file tree
Showing 8 changed files with 124 additions and 1 deletion.
29 changes: 29 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,20 @@ What's new in PyObjC

An overview of the relevant changes in new, and older, releases.

Version 10.3.1
--------------

* :issue:`610`: Ensure ``__init__`` can be used when user implements ``__new__``.

Version 10.3 dropped support for calling ``__init__``, but that breaks
a number of popular projects. Reintroduce the ability to use ``__init__``
when a class or one of its super classes contains a user implemenentation
of ``__new__``.

Code relying on the ``__new__`` provided by PyObjC still cannot use
``__init__`` for the reason explained in the 10.3 release notes.


Version 10.3
------------

Expand Down Expand Up @@ -83,6 +97,21 @@ Version 10.3
an Objective-C class, in previous versions that was the only way
to create instances in a Pythontic way.

The primairy reason for this change is that the new default ``__new__``
implementation resulted in calling ``__init__`` for some code paths and
not others due to the python semantics for creating instances, e.g.:

.. sourcecode:: python3

class MyDocument(NSDocument):
def __init__(self, *args, **kwds): pass
document = MyDocument() # __init__ gets called
document, error = MyDocument(type="mytype", error=None). # __init__ does not get called

In the last statement ``__init__`` does not get called because
``__new__`` does not return an instance of ``MyDocument``.

* ``NSArray``, ``NSMutableArray``, ``NSSet`` and ``NSMutableSet`` accepted
a ``sequence`` keyword argument in previous versions. This is no longer supported.

Expand Down
3 changes: 3 additions & 0 deletions pyobjc-core/Lib/objc/_new.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,3 +159,6 @@ def __new__(cls, *args, **kwds):
__new__.__qualname__ = cls.__name__ + ".__new__"
__new__.__module__ = cls.__module__
return function_wrapper(__new__, cls)


objc.options._genericNewClass = function_wrapper
36 changes: 35 additions & 1 deletion pyobjc-core/Modules/objc/objc-class.m
Original file line number Diff line number Diff line change
Expand Up @@ -348,14 +348,48 @@ static Class _Nullable objc_metaclass_locate(PyObject* meta_class)
static PyObject* _Nullable class_call(PyObject* self, PyObject* _Nullable args, PyObject* _Nullable kwds)
{
PyTypeObject* type = (PyTypeObject*)self;
PyObject* result;

if (type->tp_new == NULL) {
PyErr_Format( PyExc_TypeError,
"cannot create '%s' instances", type->tp_name);
return NULL;
}

return type->tp_new(type, args, kwds);

result = type->tp_new(type, args, kwds);
if (result == NULL) {
return result;
}

if (PyObject_TypeCheck(result, type) == 0) {
return result;
}

if (PyObjC_genericNewClass != NULL && PyObjC_genericNewClass != Py_None) {
PyObject* new = PyObject_GetAttr((PyObject*)type, PyObjCNM___new__);
if (new == NULL) { /* Shouldn't happen */
Py_DECREF(result);
return NULL;
}

int r = PyObject_TypeCheck(new, (PyTypeObject*)PyObjC_genericNewClass);
Py_DECREF(new);
if (r != 0) {
return result;
}
}

/* Only call __init__ when the generic new implementation is not used */

type = Py_TYPE(result);
if (type->tp_init != NULL) {
int res = type->tp_init(result, args, kwds);
if (res == -1) {
Py_SETREF(result, NULL);
}
}
return result;
}


Expand Down
1 change: 1 addition & 0 deletions pyobjc-core/Modules/objc/objc_util.h
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ extern PyObject* PyObjCNM___get__;
extern PyObject* PyObjCNM_date_format_string;
extern PyObject* PyObjCNM_objc_memview_object;
extern PyObject* PyObjCNM_objc_NULL;
extern PyObject* PyObjCNM___new__;

extern PyObject* _Nullable PyObjC_CallCopyFunc(PyObject* arg);
extern PyObject* _Nullable PyObjC_CallDecoder(PyObject* cdr, PyObject* setValue);
Expand Down
2 changes: 2 additions & 0 deletions pyobjc-core/Modules/objc/objc_util.m
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
PyObject* PyObjCNM_date_format_string;
PyObject* PyObjCNM_objc_memview_object;
PyObject* PyObjCNM_objc_NULL;
PyObject* PyObjCNM___new__;

int
PyObjCUtil_Init(PyObject* module)
Expand Down Expand Up @@ -84,6 +85,7 @@
NEW_STR(PyObjCNM_date_format_string, "%s");
NEW_STR(PyObjCNM_objc_memview_object, "objc.memview object");
NEW_STR(PyObjCNM_objc_NULL, "objc.NULL");
NEW_STR(PyObjCNM___new__, "__new__");

#undef NEW_STR

Expand Down
1 change: 1 addition & 0 deletions pyobjc-core/Modules/objc/options.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ extern PyObject* _Nullable PyObjC_setKeyPath;
extern PyObject* _Nullable PyObjC_transformAttribute;
extern PyObject* _Nullable PyObjC_processClassDict;
extern PyObject* _Nullable PyObjC_setDunderNew;
extern PyObject* _Nullable PyObjC_genericNewClass;

extern PyObject* _Nullable PyObjC_DictLikeTypes;
extern PyObject* _Nullable PyObjC_ListLikeTypes;
Expand Down
3 changes: 3 additions & 0 deletions pyobjc-core/Modules/objc/options.m
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@
OBJECT_PROP(_transformAttribute, PyObjC_transformAttribute, NULL)
OBJECT_PROP(_processClassDict, PyObjC_processClassDict, NULL)
OBJECT_PROP(_setDunderNew, PyObjC_setDunderNew, NULL)
OBJECT_PROP(_genericNewClass, PyObjC_genericNewClass, NULL)

static PyObject*
bundle_hack_get(PyObject* s __attribute__((__unused__)),
Expand Down Expand Up @@ -303,6 +304,8 @@
"Private helper used for splitting a class dict into parts"),
GETSET(_setDunderNew,
"Private helper used for setting __new__ of a new Python subclass"),
GETSET(_genericNewClass,
"Class of the generic __new__ implementation"),
{
.name = "deprecation_warnings",
.get = deprecation_warnings_get,
Expand Down
50 changes: 50 additions & 0 deletions pyobjc-core/PyObjCTest/test_subclass.py
Original file line number Diff line number Diff line change
Expand Up @@ -1155,3 +1155,53 @@ class OC_TestSuperUsage3(NSObject):
def init(self):
self = super().init()
return self


class OC_DunderInitBase(NSObject):
# Helper for ``TestUsingDunderInit``
def __new__(cls, *args, **kwds):
return cls.alloc().init()


class TestUsingDunderInit(TestCase):
# Some users have an intermediate class
# which implements ``__new__`` to be able
# to create Cocoa sublcasses using a similar
# interface as normal Python subclasses, e.g.
# with ``__init__`` for initializing the instance.
#
# This should continue to work.

def test_using_dunder_init(self):
class OC_DunderInitSub1(OC_DunderInitBase):
def __init__(self, x, y=2):
self.x = x
self.y = y

o = OC_DunderInitSub1(x=1)
self.assertIsInstance(o, OC_DunderInitSub1)
self.assertEqual(o.x, 1)
self.assertEqual(o.y, 2)

with self.assertRaises(TypeError):
OC_DunderInitSub1()

with self.assertRaises(TypeError):
OC_DunderInitSub1(9, z=4)

def test_multipe_generations(self):
class OC_DunderInitSub2(OC_DunderInitBase):
def __init__(self, x, y):
self.x = x
self.y = y

class OC_DunderInitSub3(OC_DunderInitSub2):
def __init__(self, x, y, z):
super().__init__(x, y)
self.z = z

o = OC_DunderInitSub3(1, 2, 3)
self.assertIsInstance(o, OC_DunderInitSub3)
self.assertEqual(o.x, 1)
self.assertEqual(o.y, 2)
self.assertEqual(o.z, 3)

0 comments on commit d9280a2

Please sign in to comment.