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.
Getting the parent of a dynamically method as a function
-
I have revisited this topic and this whole thread, and have decided to turn my coat on the proxy thing. Now I think that with a simple utility function, derived from what @Phuket2 presented earlier in the thread, I can have the same clean, inheritance-friendly extension mechanism, which will also play nice with add_subview and ObjCInstance.
In concrete terms, here is the utility function:
import types def extend(this, with_that): items = [(i, getattr(with_that, i)) for i in dir(with_that) if not i.startswith('__')] for k, v in items: if callable(v): setattr(this, k, types.MethodType(v.__func__, this)) else: setattr(this, k, v) return this
With that, we can have just as clean custom component code as with the proxies, with the added benefit of the object still being the original ui component. (Different ways to set style properties used for demonstration.)
# coding: utf-8 import ui class DefaultStyle(object): background_color = '#fff7ee' tint_color = 'black' def __init__(self): self.action = self.click_handler self.click_color = 'blue' def click_handler(self, sender): self.tint_color = self.click_color class HighlightStyle(DefaultStyle): def __init__(self): super(HighlightStyle, self).__init__() self.tint_color = 'red' view = ui.View() button = extend(ui.Button(), HighlightStyle()) button.title = 'Styled button' view.add_subview(button) view.present() button.frame = view.bounds.inset(300, 100)
Note that new-style classes are needed, i.e. must inherit from
object
. -
@mikael , this looks good. I think I am doing something stupid through. I can not get the button to appear in the ui.View
I changed things around a little, just to help me test.I am setting the buttons x,y, attrs in different places. I was just trying to get it to work. When I do a print dir(button) it looks fine. I can see the added attrs I threw in the extend func are there.
I am sure I am missing something very stupid. Over thinking this now.
import types import ui def extend(this, with_that): items = [(i, getattr(with_that, i)) for i in dir(with_that) if not i.startswith('__')] for k, v in items: if callable(v): setattr(this, k, types.MethodType(v.__func__, this)) else: setattr(this, k, v) # add some attrs this.my_center = (0,0) this.xxx = 0 return this class DefaultStyle(object): background_color = '#fff7ee' tint_color = 'black' def __init__(self): self.action = self.click_handler self.click_color = 'blue' def click_handler(self, sender): self.tint_color = self.click_color class HighlightStyle(DefaultStyle): def __init__(self): super(HighlightStyle, self).__init__() #DefaultStyle.__init__(self) self.tint_color = 'red' self.border_width = 1 self.bg_color = 'yellow' self.x = 10 self.y = 10 if __name__ == '__main__': f = (0,0,500,500) v = ui.View(frame = f) v.bg_color = 'white' button = extend(ui.Button(name = 'test'), HighlightStyle()) print dir(button) button.title = 'Styled button' button.x = 10 button.y = 10 button.border_width = .5 v.add_subview(button) print button.superview v.present('sheet')
-
I think the button has to have a size before it is visible, not just location. E.g. with
size_to_fit()
. -
@mikael , thanks. That was it. I wrote a 3 a4 page rant, not to you , but about being frustrated. But I decided to delete it 😱😬
Also not to to @omz. Ok, I will shut up now.
But again thanks. I have to also think about every I was trying to squeeze out of this idea and see if it carries through.One thought, I have never used ... As a param before. But if the -
def extend(this, with_that): Could be written something like
def extend(this, ...):
So you could pass x number of classes to extend to function could be a good idea. Eg, I added some attrs in the extend function, just as a test. But if I had a class MyCustomAttrs
I could possibly write-
def extend(this, with_style, with_attrs) etc...
As I say, I have never used ... As a param. I just assume that's the idea of it. Sure, you still have to do the right thing in the extend function. So you could pass it a list of objects to iterate over. This just seems nicer -
That sounds interesting and certainly feasible. Could you please open up a bit more what you would want to happen?
- Expand several target instances with one "expander"
- Expand one target instance with several "expanders"
- Apply additional attributes to the target instance
- Only apply the listed attributes from the "expander"
- Something else
-
@mikael , honestly, I am not sure yet. But I can see if you do
button = extend(ui.Button(), HighlightStyle())
Then have to do
button = extend(ui.Button(), MyExtraAttrs())Just saying if you can pass in multiple classes , it would be nicer in my mind. Ok, have a precedence issue. But it makes sense that the last class passed would take precedence as it would if you did it line by line.
Again, I am not really sure the best way. I just think a solution like this has to be very compelling to make it worth wild
-
@Phuket2, that sounds like a wothwhile convenience feature. Here is a version that let's you apply several "expanders" with one call:
# coding: utf-8 import types # Extend the instance given as the first argument with the methods and properties of the instances given as the second and subsequent arguments def extend(target, *args): for source in args: for key in dir(source): if key.startswith('__'): continue value = getattr(source, key) if callable(value): setattr(target, key, types.MethodType(value.__func__, target)) else: setattr(target, key, value) return target
-
And sample usage/test case:
# coding: utf-8 import ui from extend import extend class DefaultStyle(object): background_color = '#fff7ee' tint_color = 'black' class HighlightStyle(DefaultStyle): def __init__(self): super(HighlightStyle, self).__init__() self.tint_color = 'red' class ClickHandler(object): def __init__(self): self.action = self.click_handler self.click_color = 'blue' def click_handler(self, sender): self.tint_color = self.click_color view = ui.View() button = extend(ui.Button(), HighlightStyle(), ClickHandler()) button.title = 'Styled button' view.add_subview(button) view.present() button.frame = view.bounds.inset(300, 100)
-
@mikael nice. Yes. I just tested also.
All my idea is that it needs to be very compelling and super useful to make sense that other will use it.
Getting late here now, I will try and push it more tomorrow.import types import ui # Extend the instance given as the first argument with the methods and properties of the instances given as the second and subsequent arguments def extend(target, *args): for source in args: for key in dir(source): if key.startswith('__'): continue value = getattr(source, key) if callable(value): setattr(target, key, types.MethodType(value.__func__, target)) else: setattr(target, key, value) return target class DefaultStyle(object): background_color = '#fff7ee' tint_color = 'black' def __init__(self): self.action = self.click_handler self.click_color = 'blue' def click_handler(self, sender): self.tint_color = self.click_color class HighlightStyle(DefaultStyle): def __init__(self): super(HighlightStyle, self).__init__() #DefaultStyle.__init__(self) self.tint_color = 'red' self.border_width = 1 self.bg_color = 'yellow' self.x = 10 self.y = 10 class MyCustomAttrs(object): def __init__(self): self.xzy = 666 if __name__ == '__main__': f = (0,0,500,500) v = ui.View(frame = f) v.bg_color = 'white' button = extend(ui.Button(name = 'test'), HighlightStyle(), MyCustomAttrs()) print dir(button) button.title = 'Styled button' button.size_to_fit() button.x = 10 button.y = 10 button.border_width = .5 v.add_subview(button) print button.superview v.present('sheet')
-
@mikael , but again for the idea to be relevant it has to compete with something like this. Looks like more code then we have been doing. But when you look at the one off code it's not.
Just saying that's how I am trying to evaluate if it's worth while or not# coding: utf-8 import ui def MyMother(sender): print 'I love my Mums cooking' _default_btn_style = \ { 'border_width' : .5, 'corner_radius' : 3, 'bg_color' : 'teal', 'tint_color' : 'white', 'action' : MyMother, } _btn_style = _default_btn_style _extra_attrs = \ { 'myx' : 0, 'myy' : 0, } def make_button(style = _btn_style, ext_attrs = _extra_attrs , *args, **kwargs): btn = ui.Button() # process the normal atttrs for k,v in kwargs.iteritems(): if hasattr(btn, k): setattr(btn, k, v) # process a style dict for k,v in style.iteritems(): if hasattr(btn, k): setattr(btn, k, v) # process addtional attrs... for k,v in ext_attrs.iteritems(): setattr(btn, k, v) # if kwargs has a parent key, then we add the subview to the parent if kwargs.has_key('parent'): kwargs['parent'].add_subview(btn) btn.size_to_fit() # size to fit is too tight btn.frame = ui.Rect(*btn.bounds).inset(0, -5) return btn if __name__ == '__main__': f = (0,0,500,500) v = ui.View(frame = f) btn = make_button(title = 'What do I Like', parent = v) v.present('sheet')
-
I know this is an older post, but I am wondering why no one recommended using a button action?
def test_func(sender): return sender v = ui.load_view() button = v['button1'] button.action = test_func
Then the only question would be how to pass parameters to a button's action function.
"sender" is the button that called the function. -
@TutorialDoctor, we are not focused on the button, but on reasonably elegant ways to get around the fact that we can not subclass ui components. Button is just being used as a shared sample, so to say.
-
@Phuket2, to exaggerate a bit, it looks like where I have been working towards the most object-oriented, legible and maintainable way to create custom components, emphasizing maybe a low number of complex components, you might have been working on the most flexible and time-saving functional-programming button maker, with a possible further focus on generating relatively many less-complex components easily.
Pretty opposite ends of the spectrum, one could say, but nevertheless this has been very instructive and interesting, and I have learned a lot about Python meta-programming in the process. Thanks!
Out of curiosity, I will try and see what a roughly similar generator would look like in "my style".
-
@mikael , also thanks. I have also enjoyed it and learnt a lot. For me it's about remembering what I have learnt 😳
But I think I am starting to learn something that seems so obvious, but not really. We I guess depending on your experience. But that is to really get clear what you are trying to improve on. I can see I go off on all sort of tangents trying to solve problems that either don't exists, I just keep working on something without looking back and comparing the methods, basically having a bench mark.
I still have some years left in me to learn 😁 So all good.
-
@TutorialDoctor , yes this thread has been around the would and back.
-
@Phuket2, here's the latest. I went a bit further with the meta stuff and created an Extender base class for functionality wrappers.
import types class Extender(object): def __new__(cls, target_instance, *args, **kwargs): if isinstance(cls.__init__, types.MethodType): cls.__init__.__func__(target_instance, *args, **kwargs) extender_instance = super(Extender, cls).__new__(cls) for key in dir(extender_instance): if key.startswith('__'): continue value = getattr(extender_instance, key) if callable(value): setattr(target_instance, key, types.MethodType(value.__func__, target_instance)) else: setattr(target_instance, key, value) return target_instance
-
Using the Extender base, an attempt at apples-to-apples comparison code, matching the code you shared. ButtonFactory is perhaps not exactly how I would create it in isolation, but it acts as a useful test case for passing arguments through the custom new.
# coding: utf-8 import ui from extend import Extender def MyMother(sender): print 'I love my Mums cooking' class DefaultStyle(Extender): border_width = .5 corner_radius = 3 background_color = 'teal' tint_color = 'white' class ButtonFactory(Extender): def __init__(self, parent = None, position = (0,0), **kwargs): if parent: parent.add_subview(self) self.size_to_fit() (self.x, self.y) = position self.width += 10 self.action = MyMother for key, value in kwargs.iteritems(): setattr(self, key, value) view = ui.View(frame = (0,0,500,500)) button = ButtonFactory( DefaultStyle( ui.Button(title = 'What do I like?')), parent = view, tint_color = 'yellow') view.present('sheet')
-
This version of the extension functionality now meets mostnof my design goals:
- Use standard class syntax for encapsulating data and functionality - in my case, much preferred to the dict syntax
- Can extend ui component functionality, "almost subclassing"
self
always refers to the instance being extended, also in__init__
- Constructor returns the instance being extended instead of the extender, which allows for chaining extenders
- Since the original UI class instance is returned, can be used in
add_subclass
andObjCInstance
These are still a fail:
-
Extra
__func__
is needed when calling overloaded methods from superclass, most often done in__init__
:SuperClass.__init__.__func__(self, params)
-
@mikael , I didn't get a chance to run your yet. I have been trying to get another thing working.but during the course of getting the other thing working I wanted a copy ui element function. I have only tried the below with a button. I am sure , there are some things in textfields etc that cause exceptions. That's why I made the exclude list. Maybe it's not great, but it's a idea anyway. Many times I just want a copy of a object with a few changes , normally just X and y and maybe title. But copy and deepcopy fail. I haven't tried them for a while actually. Of course that would be better as part of the solution.
def copy_obj(obj , **kwargs): new_obj = type(obj)() # copy the attrs from the passed object to the new object attr_exclude_list = ['left_button_items', 'right_button_items', 'navigation_view', 'on_screen', 'subviews', 'superview'] # is removed in the list comp because is callable. we add it back :) attr_include_list = ['action'] intrested_attrs = [k for k in dir(obj) if not k.startswith('_') and not callable(getattr(obj, k)) and k not in attr_exclude_list ] + attr_include_list for k in intrested_attrs: if hasattr(new_obj, k): print k, getattr(obj, k) setattr(new_obj, k, getattr(obj, k)) # overwite new attrs in the new object ,passed in **kwargs for k, v in kwargs.iteritems(): if hasattr(new_obj, k): setattr(new_obj, k, v) return new_obj
-
@mikael , the copy_obj should be more complete now. There is a test function also. But trying this on ui.NavigationView or ui.ButtonItem ends in tears 😱
But there is a list of elements it can get through. Also a quirky thing about unassigned attrs. Have to check for them. If not already assigned in the source object to copy, some fail. As source attr has a value of None, but the target is expecting a different type. I haven't really handled this well at the moment. I have done something.
Anyway work in progress, the print statements are still in
import warnings def copy_obj(obj , **kwargs): # an idea for ui object copying... # a list of object types we accept ui_can_copy = [ui.View, ui.Button, ui.Label, ui.TextField, ui.TextView, ui.TableView, ui.ImageView, ui.SegmentedControl, ui.ScrollView] if type(obj) not in ui_can_copy: warnings.warn('Can not copy object {}'.format(type(obj))) return new_obj = type(obj)() # copy the attrs from the passed object to the new object attr_exclude_list = ['left_button_items', 'right_button_items', 'navigation_view', 'on_screen', 'subviews', 'superview', 'content_offset', 'content_size'] # is removed in the list comp because is callable. we add it back :) attr_include_list = ['action'] intrested_attrs = [k for k in dir(obj) if not k.startswith('_') and not callable(getattr(obj, k)) and k not in attr_exclude_list ] + attr_include_list for k in intrested_attrs: if hasattr(new_obj, k): print k, type(getattr(obj, k)) attr = getattr(obj, k) ''' This is an important test. if we try to copy a string attr that has nit been previously set, it will fail if we do a blind copy/assignment ''' if attr: setattr(new_obj, k, attr) # overwite new attrs in the new object ,passed in **kwargs for k, v in kwargs.iteritems(): if hasattr(new_obj, k): setattr(new_obj, k, v) return new_obj def test_copy_obj(): # notice there us No navigation_view also no ui.ButtonItem ui_can_copy = [ui.View, ui.Button, ui.Label, ui.TextField, ui.TextView, ui.TableView, ui.ImageView, ui.SegmentedControl, ui.ScrollView] for obj in ui_can_copy: o = obj() print type(o) x = copy_obj(o)