objc_classes.py
from objc_util import * | |
import weakref | |
import functools | |
''' Attempt at making more natural objc class in pythonista. | |
decorate python class with @objclass to create an ObjCClass. | |
set superclass and protocols class attributes, if desired. | |
decorate objc methods with @objc_method. | |
skip the _self, and _sel fields, these will be genrated by the decorator. | |
use @objc_method(type_encoding), when using non-protocol methods, or if args are not all c_void_p. | |
in cases where type_encoding is screwed up by pythonista, can use annotations to override argtypes/restype. | |
the decorator automatically replaces the objc instance with the python obj self. | |
self._objc_instance returns the objc instance | |
instances of the python instance must be retained while the objc instance is alive, otherwise an exception will be raised. | |
e.g | |
@objcclass | |
class MySearchResultUpdater(object): | |
protocol=['UISearchControllerUpdating'] | |
superclass=NSObject | |
def __init_(self): | |
self.tv=None | |
@objcmethod | |
def updateSearchResultsForSearchController_(self, controller:c_void_p)->None: | |
#creates updateSearchResultsForSearchController_(_self,_sel,controller) | |
tv=self.tv #instance variable! | |
if ObjCInstance(controller).active(): | |
sb=ObjCInstance(controller).searchBar() | |
filterTerm=str(sb.text()) | |
tv.data_source.filter_items(filterTerm) | |
else: | |
tv.data_source.filter_items('') | |
''' | |
def get_tagged_methods(cls,tagname='_objcmethod'): | |
'''find all tagged methods. ''' | |
methods=[] | |
for m in cls.__dict__.values(): | |
try: | |
if getattr(m,tagname): | |
methods.append(m) | |
except AttributeError: | |
pass | |
return methods | |
def objc_class(cls): | |
'''decorator which creates an objcclass. | |
creates the associated class, and hijacks init method to associate instances with each other, and register a weakref finalizer. | |
''' | |
methods=get_tagged_methods(cls,'_objcmethod') | |
if hasattr(cls,'superclass'): | |
superclass=cls.superclass | |
else: | |
superclass=ObjCClass('NSObject') | |
if hasattr(cls,'protocols'): | |
protocols=cls.protocols | |
else: | |
protocols=[] | |
cls._objcclass=create_objc_class(cls.__name__, | |
superclass=superclass, | |
methods=methods, | |
protocols=protocols, debug=True) | |
oldinit=cls.__init__ | |
def __init__(self,*args,**kwargs): | |
self._objc_instance=cls._objcclass.new() | |
self._objc_ptr=self._objc_instance.ptr | |
set_associated_object(self._objc_instance,self) | |
#idea for finalizers, which won't really work -- as f cannot refernce self. | |
finalizers=get_tagged_methods(self,'_objc_finalizer') | |
for f in finalizers: | |
weakref.finalize(self,f) | |
#register finalizer to degregister the py obj, so if objc methods get called, the objc_method decorator will raise an exception rather than do unexpected things | |
weakref.finalize(self,set_associated_object,self._objc_instance,None) | |
oldinit(self, *args, **kwargs) | |
cls.__init__=__init__ | |
return cls | |
def get_associated_object(cobj, key='pyObject'): | |
'''get the python object associated with an objc object''' | |
#import here, so global clearing doesnt screw it up | |
from objc_util import ns, sel, ObjCInstance, c | |
from ctypes import c_void_p, c_ulong, py_object, cast | |
objc_getAssociatedObject=c.objc_getAssociatedObject | |
objc_getAssociatedObject.argtypes=[c_void_p, c_void_p] | |
objc_getAssociatedObject.restype=c_void_p | |
pyobj_addr=objc_getAssociatedObject(ObjCInstance(cobj),sel(key)) | |
pyobj=cast(ObjCInstance(pyobj_addr).longValue(),py_object) | |
return pyobj.value | |
def set_associated_object(cobj,pyobj,key='pyObject'): | |
'''set a python object associated with an objc object. | |
you MUST hang onto the python object reference unless you set a new oject for key (use None to deregister) | |
''' | |
from objc_util import ns, sel, ObjCInstance, c | |
from ctypes import c_void_p, c_ulong | |
objc_setAssociatedObject=c.objc_setAssociatedObject | |
objc_setAssociatedObject.argtypes=[c_void_p, c_void_p, c_void_p, c_ulong] | |
objc_setAssociatedObject.restype=None | |
pyobj_addr=id(pyobj) | |
#print('setting obj',cobj,pyobj) | |
objc_setAssociatedObject(ObjCInstance(cobj),sel(key),ns(pyobj_addr),3) | |
def build_argtypes_for_annotations(argspec): | |
''' This was an idea to allow type annotations to be used in objc method declarations. | |
given type annotations, create argtypes and restype. however, currently, objc_util ignores argtypes/restype in method attributes, if an encoding is set, such as in protocols. better just to use encoding''' | |
import inspect | |
argtypes=[] | |
restype=None | |
if argspec.annotations: | |
for arg in argspec.args[1:]: | |
argtypes.append(argspec.annotations.get(arg,c_void_p)) | |
restype=argspec.annotations.get('return',None) | |
return argtypes, restype | |
def objc_method(encoding): | |
'''@objc_method(encoding) creates on objc method with the given type encoding. | |
type encoding is not needed if using protocol, or if using type annotations. | |
the subsequent def should omit the hidden _self,_sel args, just use self. | |
objc will see (_self,_sel,arg1...), while python gets called with (self,arg1...), using instance looked up from the associated object | |
''' | |
def method_wrapper(fcn): | |
# create new method that creates a method signature compatible with objc | |
import inspect | |
argspec=inspect.getfullargspec(fcn) | |
#this feels hacky, but seems necessary to create proper fcn prototype with different argspec | |
code_to_compile='''def {fcnname}(_self,_cmd,{args}): | |
self=get_associated_object(_self) | |
if not self: | |
raise('Python Object no longer exists') | |
#print({args}) | |
return fcn(self, {args}) | |
'''.format(fcnname=fcn.__name__, args=','.join(argspec.args[1:])) | |
locs={'fcn':fcn} | |
globs={'get_associated_object':get_associated_object, 'fcn':fcn} | |
exec(code_to_compile,globs,locs) | |
new_fcn=locs[fcn.__name__] | |
new_fcn._objcmethod=True | |
new_fcn.__annotations__=fcn.__annotations__.copy() | |
argtypes,restype=build_argtypes_for_annotations(argspec) | |
new_fcn.__annotations__={} | |
if argtypes: | |
new_fcn.argtypes=argtypes | |
new_fcn.restype=restype | |
if encoding: | |
new_fcn.encoding=encoding | |
return new_fcn | |
return method_wrapper | |
def objc_block(argtypes=[c_void_p], restype=None): | |
'''TODO... use type annotations to detect argtypes,etc. ''' | |
def wrapper(func): | |
return ObjCBlock(func,argtypes=argtypes,restype=restype) | |
return wrapper | |
if __name__=='__main__': | |
@objc_class | |
class TestClass(object): | |
def __init__(self): | |
self.value=5 | |
def cleanup(): | |
print('finalize!') | |
weakref.finalize(self, cleanup) | |
@objc_method('v@:if') | |
def addValueToInteger_TimesFloat_(self, intvalue, floatvalue): | |
print('{}+{}*{}={}'.format(self.value,+intvalue,floatvalue, self.value+intvalue*floatvalue)) | |
t=TestClass() | |
t._objc_instance.addValueToInteger_TimesFloat_(3,2) | |
t.value=10 | |
t._objc_instance.addValueToInteger_TimesFloat_(3,2) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment