Skip to content

Instantly share code, notes, and snippets.

@jsbain
Created February 12, 2019 07:59
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jsbain/e3ea36bfd652e7ace38987714c01622c to your computer and use it in GitHub Desktop.
Save jsbain/e3ea36bfd652e7ace38987714c01622c to your computer and use it in GitHub Desktop.
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