Created
February 12, 2019 07:59
-
-
Save jsbain/e3ea36bfd652e7ace38987714c01622c to your computer and use it in GitHub Desktop.
objc_classes.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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