Commits

Ronald Oussoren committed c02ee09

Add a decorator "objc.python_method"

This decorator can be used to add method to a class
that won't be converted to an Objective-C selector,
which can be useful for adding a more Pythonic API
for classes that are subclasses of NSObject because
they need to interact with the Objective-C world.

Comments (0)

Files changed (8)

pyobjc-core/Doc/api/module-objc.rst

       Returns a copy of the metadata dictionary for the selector.  See the
       :doc:`metadata system documentation </metadata/manual>` for more information.
 
+.. class:: python_method(callable)
+
+   Use this as a decorator in a Cocoa class definition to avoid creating a
+   selector object for a method.
+
+   For example::
+
+       class MyClass (NSObject):
+
+          @python_method
+          @classmethod
+          def fromkeys(self, keys):
+              pass
+
+          @python_method
+          def items(self):
+              pass
+
+   In this example class *MyClass* has a Python classmethod "fromkeys" and
+   a normal method "items", neither of which are converted to a selector object
+   and neither of which are registered with the Objective-C runtime.
+
+   Instances of this type have an attribute named *callable* containing the wrapped
+   callable, but are themselves not callable.
+
+   .. note::
+
+      If you use multiple decorators the :class:`python_method` decorator should be
+      the outermost decorator (that is, the first one in the list of decorators).
+
 .. class:: ivar([name[, type[, isOutlet]]])
 
    Creates a descriptor for accessing an Objective-C instance variable. This should only

pyobjc-core/Modules/objc/module.m

     if (PyType_Ready(&PyObjC_FSSpecType) < 0) {
         PyObjC_INITERROR();
     }
+    if (PyType_Ready(&PyObjCPythonMethod_Type) < 0) {
+        PyObjC_INITERROR();
+    }
 
 #ifndef Py_HAVE_LOCAL_LOOKUP
     PyObjCSuper_Type.tp_doc = PySuper_Type.tp_doc;
     if (PyDict_SetItemString(d, "IMP", (PyObject*)&PyObjCIMP_Type) < 0) {
         PyObjC_INITERROR();
     }
+    if (PyDict_SetItemString(d, "python_method", (PyObject*)&PyObjCPythonMethod_Type) < 0) {
+        PyObjC_INITERROR();
+    }
 
 #ifndef Py_HAVE_LOCAL_LOOKUP
     if (PyDict_SetItemString(d, "super", (PyObject*)&PyObjCSuper_Type) < 0) {

pyobjc-core/Modules/objc/pyobjc.h

 #include "objc-class.h"
 #include "objc-object.h"
 #include "selector.h"
+#include "python-method.h"
 #include "libffi_support.h"
 #include "super-call.h"
 #include "instance-var.h"

pyobjc-core/Modules/objc/python-method.h

+#ifndef PyObjC_PYTHON_METHOD_H
+#define PyObjC_PYTHON_METHOD_H
+
+extern PyTypeObject PyObjCPythonMethod_Type;
+#define PyObjCPythonMethod_Check(obj) PyObject_TypeCheck(obj, &PyObjCPythonMethod_Type)
+
+extern PyObject* PyObjCPythonMethod_GetMethod(PyObject*);
+
+#endif /* PyObjC_PYTHON_METHOD_H */
+

pyobjc-core/Modules/objc/python-method.m

+#include "pyobjc.h"
+
+typedef struct {
+    PyObject_HEAD
+
+    PyObject* callable;
+} PyObjCPythonMethod;
+
+
+PyObject*
+PyObjCPythonMethod_GetMethod(PyObject* value)
+{
+    if (!PyObjCPythonMethod_Check(value)) {
+        PyErr_SetString(PyExc_TypeError, "Expecting a python-method object");
+        return NULL;
+    }
+
+    return ((PyObjCPythonMethod*)value)->callable;
+}
+
+static PyMemberDef meth_members[] = {
+    {
+        .name   = "callable",
+        .type   = T_OBJECT,
+        .offset = offsetof(PyObjCPythonMethod, callable),
+        .flags  = READONLY,
+    },
+    {
+        .name   = NULL  /* SENTINEL */
+    }
+};
+
+
+static PyObject*
+meth_new(PyTypeObject* type __attribute__((__unused__)),
+        PyObject* args, PyObject* kwds)
+{
+static char* keywords[] = { "callable", NULL };
+    PyObject* callable;
+    PyObjCPythonMethod* result;
+
+    if (!PyArg_ParseTupleAndKeywords(args, kwds, "O", keywords, &callable)) {
+        return NULL;
+    }
+
+    result = (PyObjCPythonMethod*)PyObject_New(PyObjCPythonMethod, &PyObjCPythonMethod_Type);
+    if (result == NULL) {
+        return NULL;
+    }
+    result->callable = callable;
+    Py_INCREF(callable);
+
+    return (PyObject*)result;
+}
+
+static PyObject*
+meth_descr_get(PyObject* self, PyObject* obj, PyObject* class)
+{
+    descrgetfunc f;
+    PyObject* result;
+
+    result = ((PyObjCPythonMethod*)self)->callable;
+    if (unlikely(result == NULL)) {
+        PyErr_SetString(PyExc_ValueError, "Empty objc.python-method");
+        return NULL;
+    }
+    f = Py_TYPE(result)->tp_descr_get;
+    if (f == NULL) {
+        Py_INCREF(result);
+    } else {
+        result = f(result, obj, class);
+    }
+    return result;
+}
+
+static int
+meth_traverse(PyObject* self, visitproc visit, void* arg)
+{
+    return visit(((PyObjCPythonMethod*)self)->callable, arg);
+}
+
+static int
+meth_clear(PyObject* self)
+{
+    Py_CLEAR(((PyObjCPythonMethod*)self)->callable);
+    return 0;
+}
+
+static void
+meth_dealloc(PyObject* self)
+{
+    meth_clear(self);
+    Py_TYPE(self)->tp_free(self);
+}
+
+PyDoc_STRVAR(meth_doc,
+    "objc.python_method(callable)\n"
+    CLINIC_SEP
+    "\nReturns a descriptor that won't be converted to a selector object \n"
+    "and won't be registered with the Objective-C runtime.\n"
+    );
+
+
+PyTypeObject PyObjCPythonMethod_Type = {
+    PyVarObject_HEAD_INIT(&PyType_Type, 0)
+    .tp_name        = "objc.python_method",
+    .tp_basicsize   = sizeof(PyObjCPythonMethod),
+    .tp_itemsize    = 0,
+    .tp_dealloc     = meth_dealloc,
+    .tp_getattro    = PyObject_GenericGetAttr,
+    .tp_flags       = Py_TPFLAGS_DEFAULT,
+    .tp_doc         = meth_doc,
+    .tp_members     = meth_members,
+    .tp_new         = meth_new,
+    .tp_descr_get   = meth_descr_get,
+    .tp_traverse    = meth_traverse,
+    .tp_clear       = meth_clear,
+};

pyobjc-core/NEWS.txt

 Version 3.0
 -----------
 
+* Added a decorator "python_method" than can be used to decorate methods that should
+  not be registered with the Objective-C runtime and should not be converted to a
+  Objective-C selector.
+
+  Usage::
+
+      class MyClass (NSObject):
+
+          @python_method
+	  @classmethod
+	  def fromkeys(self, keys):
+	      pass
+
+  This makes it easier to add a more "pythonic" API to Objective-C subclasses without
+  being hindered by PyObjC's conventions for naming methods.
+
 * Issue #64: Fix metadata for ``Quartz.CGEventKeyboardSetUnicodeString``
   and ``Quartz.CGEventKeyboardGetUnicodeString``.
 

pyobjc-core/PyObjCTest/test_python_method.py

+from PyObjCTools.TestSupport import *
+import objc
+
+NSObject = objc.lookUpClass('NSObject')
+
+class TestPythonMethod (TestCase):
+    def test_creation(self):
+        self.assertRaises(TypeError, objc.python_method)
+        self.assertRaises(TypeError, objc.python_method, 1, 2)
+        o = objc.python_method(1)
+        self.assertEqual(o.callable, 1)
+
+    def test_usage_basic(self):
+
+        class MyClass (object):
+            @objc.python_method
+            def my_method(self, a):
+                return a * 2
+
+            b = objc.python_method(1)
+
+            @objc.python_method
+            @classmethod
+            def my_class(cls):
+                return str(cls)
+
+        o = MyClass()
+        self.assertEqual(o.my_method(4), 8)
+        self.assertEqual(o.b, 1)
+
+        self.assertEqual(MyClass.my_class(), str(MyClass))
+
+    def test_usage_objc(self):
+        class OC_PythonMethod_Class (NSObject):
+            @objc.python_method
+            def my_method(self, a):
+                return a * 2
+
+            def someSelector(self):
+                pass
+
+            b = objc.python_method(2)
+
+        o = OC_PythonMethod_Class.alloc().init()
+        self.assertIsNotInstance(o.my_method, objc.selector)
+        self.assertIsInstance(o.someSelector, objc.selector)
+
+        self.assertEqual(o.my_method(4), 8)
+        self.assertEqual(o.b, 2)
+
+if __name__ == "__main__":
+    main()
+
+from Foundation import NSObject
+from objc import python_method
+
+
+class MyObject (NSObject):
+    @python_method
+    def my_method(self):
+        return 42
+
+
+print(MyObject.my_method)
+print(MyObject.alloc().init().my_method)
+print(MyObject.alloc().init().my_method())