Forum Archive

Access PyObject From ObjCClass

[deleted]

Hello, I'm playing with creating classes with the objc_util module, and I am running into a problem. I want a class that contains objc methods that can access the underlying python object. For example,

class TestClass(object):
    def __init__(self):
        self.objcInstance = objc_util.create_objc_class(
            "TestClass",
            methods = [self.testMethod]
        ).alloc().init()

        self.testVariable = 1
        pass

    def testMethod(self, _self, _cmd):
        print(self.testVariable)
        pass

TestClass().objcInstance.testMethod()

But when I run this i get the following error,

Traceback (most recent call last):
  File "/private/var/mobile/Containers/Shared/AppGroup/08CF4A94-F824-408A-B12B-53EA02F43C24/Pythonista3/Documents/Projects/musicruntime/queueviewer.py", line 48, in <module>
    TestClass().objcInstance.testMethod()
  File "/private/var/mobile/Containers/Shared/AppGroup/08CF4A94-F824-408A-B12B-53EA02F43C24/Pythonista3/Documents/Projects/musicruntime/queueviewer.py", line 39, in __init__
    methods = [self.testMethod]
  File "/var/containers/Bundle/Application/E768493A-4B9B-48EB-82D1-FADC51CC89B6/Pythonista3.app/Frameworks/Py3Kit.framework/pylib/site-packages/objc_util.py", line 1191, in create_objc_class
    _add_method(method, class_ptr, superclass, basename, protocols)
  File "/var/containers/Bundle/Application/E768493A-4B9B-48EB-82D1-FADC51CC89B6/Pythonista3.app/Frameworks/Py3Kit.framework/pylib/site-packages/objc_util.py", line 1153, in _add_method
    raise ValueError('%s has %i arguments (expected %i)' % (method, len(argspec.args), len(argtypes)))
ValueError: <bound method TestClass.testMethod of <__main__.TestClass object at 0x1180f0f28>> has 3 arguments (expected 2)

The only I can think to solve this would be either creating a global variable or turning TestClass into a singleton, but that isn't ideal.

Thanks for any help!.

mikael

@Hw16279180, this way works, but not sure if it is moving towards your goal. If you need to, you can use metaclass to hide the instance creation within a more Pythonic class. I can share some code on that later today.


import types

import objc_util

TestClass = objc_util.create_objc_class("TestClass")

def test_method(self):
    print(self.test_variable)

def create_test_instance():
    test_instance = TestClass.alloc().init()
    setattr(test_instance, 'test_method', types.MethodType(test_method, test_instance))
    return test_instance

if __name__ == '__main__':

    instance = create_test_instance()
    instance.test_variable = 'Instance attribute'
    instance.test_method()
mikael

@Hw16279180, with this helper:

import inspect
import types
import uuid

import objc_util


class ObjCPlus:

    def __new__(cls, *args, **kwargs):
        objc_class = getattr(cls, '_objc_class', None)
        if objc_class is None:
            objc_class_name = 'TempClass'+str(uuid.uuid4())[-12:]
            objc_methods = [value
                for value in cls.__dict__.values()
                if (
                    callable(value) and 
                    '_self' in inspect.signature(value).parameters
                )
            ]
            objc_protocols = getattr(cls, '_objc_protocols', [])
            if not type(objc_protocols) is list:
                objc_protocols = [objc_protocols]
            objc_class = objc_util.create_objc_class(
                objc_class_name,
                methods=objc_methods,
                protocols=objc_protocols
            )

        instance = objc_class.alloc().init()

        for key in dir(cls):
            value = getattr(cls, key)
            if inspect.isfunction(value):
                if not '_self' in inspect.signature(value).parameters:
                    setattr(instance, key, types.MethodType(value, instance))
                if key == '__init__':
                    value(instance, *args, **kwargs)
        return instance

... your original example becomes:

class TestClass(ObjCPlus):

    def __init__(self):
        self.test_variable = 'Instance attribute'

instance = TestClass()
assert instance.test_variable == 'Instance attribute'
assert type(instance) is objc_util.ObjCInstance

A more realistic example combining different types of methods could be:

class GestureHandler(ObjCPlus):

    # Can be a single string or a list
    _objc_protocols = 'UIGestureRecognizerDelegate'

    # Vanilla Python __init__
    def __init__(self):
        self.other_recognizers = []

    # ObjC delegate method matching the protocol
    def gestureRecognizer_shouldRecognizeSimultaneouslyWithGestureRecognizer_(
            _self, _sel, _gr, _other_gr):
        self = ObjCInstance(_self)
        other_gr = ObjCInstance(_other_gr)
        return other_gr in self.other_recognizers

    # Custom ObjC action target for gesture recognizers
    def gestureAction(_self, _cmd):
        self = ObjCInstance(_self)
        ...

    # Vanilla Python method
    @objc_util.on_main_thread
    def before(self, other):
        return other.recognizer in self.other_recognizers
[deleted]

@mikael Wow! this is amazing. one question though, what is this line for?
objc_class = getattr(cls, '_objc_class', None)

mikael

@Hw16279180, if you want to build the ObjC class ”by hand” with create_objc_class, you can set that as the base class with the _objc_class class variable. Thus the previous example would become:

def gestureRecognizer_shouldRecognizeSimultaneouslyWithGestureRecognizer_(
        _self, _sel, _gr, _other_gr):
    self = ObjCInstance(_self)
    other_gr = ObjCInstance(_other_gr)
    return other_gr in self.other_recognizers

# Custom ObjC action target
def gestureAction(_self, _cmd):
    self = ObjCInstance(_self)
    ...

GestureHandlerObjC = objc_util.create_objc_class(
    'GestureHandlerObjC',
    methods=[
        gestureAction,
        gestureRecognizer_shouldRecognizeSimultaneouslyWithGestureRecognizer_,
    ],
    protocols=['UIGestureRecognizerDelegate'],
)

class GestureHandler2(ObjCPlus):

    _objc_class = GestureHandlerObjC

    # Vanilla Python __init__
    def __init__(self):
        self.other_recognizers = []

    # Vanilla Python method
    @objc_util.on_main_thread
    def before(self):
        return self.other_recognizers
mikael

@Hw16279180, if you are interested in ObjC bridging, you could also google rubicon-objc and all the nice descriptor/typing stuff ”our very own” @dgelessus and others have done there.

[deleted]

@mikael Thanks I'll look into it.