Welcome!
This is the community forum for my apps Pythonista and Editorial.
For individual support questions, you can also send an email. If you have a very short question or just want to say hello — I'm @olemoritz on Twitter.
Access PyObject From ObjCClass
-
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!.
-
@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()
-
@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
-
@mikael Wow! this is amazing. one question though, what is this line for?
objc_class = getattr(cls, '_objc_class', None)
-
@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
-
@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.
-
@mikael Thanks I'll look into it.