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.
UI proxies for subclassing
-
I'm Convinced That
ui
Was Deliberately Designed to be Unhackable: the Rant. 😛I. ProxyTypes is cool (and old)
Today I started using
ProxyTypes
to "subclass" the uninheritableui.View
classes [1] (e.g.
TableView
andSlider
), as @mikael showed a while ago. These classes delegate literally everything to the wrapped view.Take the following example:
>>> from ui2.subclassing.proxies import ObjectProxy >>> a = ui.View() >>> b = ObjectProxy(a) >>> b.background_color (0.0, 0.0, 0.0, 0.0) >>> b <_ui.View object at 0x10cbbf048> >>> isinstance(b, ui.View) True
The
ObjectProxy
makes it nearly impossible to distinguishb
froma
. The only way they can be told apart is:>>> type(b) <class 'ui2.subclassing.proxies.ObjectProxy'>
II.
ui
is uncoolThese classes can't be added as subviews:
>>> ui.View().add_subview(b) Traceback (most recent call last): File "<string>", line 1, in <module> TypeError: Expected a View
To me, it looks like
ui
is checkingissubclass(type(b), ui.View)
. Now, there's no reason whyui
couldn't have used the simplerisinstance
method unless it's deliberately trying to prevent my sort of trickery. The thing is, I've read the source ofProxyTypes
(I rewrote most of it yesterday) and it forwards everything, far more than should be necessary. I'm assuming that the_ui
library does most of its stuff off of the_objc_pointer
, which is totally forwarded and should be covered. For god's sake, evenObjCInstance
is forwarded through theObjectProxy
:>>> ObjCInstance(b) <b'SUIView_PY3': <SUIView_PY3: 0x15a1342a0; frame = (0 0; 100 100); clipsToBounds = YES; layer = <CALayer: 0x159085120>>>
In short, the
ObjectProxy
is more than robust enough, this should work. Unless @omz is using some kind of weird C type-checking I don't know about, he's going out of his way to prevent this.III. Almost solutions
These are sorted by my perceived chance of their success.
- Create a new
ObjectProxy
subclass which uses multiple inheritance to also inherit fromui.View
. The idea of this is that the class wouldn't actually act as a view itself because of the use of__getattribute__
(VERY different from__getattr__
.) This is "almost" because conflicts with__slots__
mean that I can't create anObjectProxy
subclass which uses multiple inheritance to also inherit fromui.View
. I'll see if I can take__slots__
out ofProxyTypes
, since I think they're just for performance. - Try to write a proxy with a metaclass overriding
__subclasscheck__
so that myissubclass(type(b), ui.View)
check passes. This idea is "almost" because I haven't tried it yet. I've never used metaclasses before, so I may be totally wrong about what they can do. The way I see it, there's also a good chance that__slots__
will make my iPad catch on fire when I try this.
IV. Question
My question is: why? Am I getting something wrong? Why not allow this convoluted method of subclassing the uninheritable types?
Lastly, I'd love to hear any other ideas for overcoming this besides what I've proposed.
[1] I rewrote
ProxyTypes
for PEP8 compliance and Python 3 support (As of tomorrow, it will not have been updated in 10 years 😱) The new version is inui2
and there's a good chance I got the licensing completely wrong. - Create a new
-
Please, I really doubt that omz is actively trying to stop you from subclassing certain classes. Most likely these things are just a consqeucene of how Python's C API works. First of all, subclassing needs to be explicitly enabled on the C side. When defining a Python class in C, you basically create a big
PyTypeObject
(which is astruct
) containing all info about the class. One of thestruct
fields istp_flags
, which contains various flags (duh). For example the one fromPyFloat_Type
looks like this:Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /* tp_flags */
And for comparison, the one from
PySlice_Type
:Py_TPFLAGS_DEFAULT, /* tp_flags */
Note how the one for
float
hasPy_TPFLAGS_BASETYPE
while the one forslice
does not. And if you try it in Python, you'll see that you can subclassfloat
, but forslice
Python will tell you that it's "not an acceptable base type".And why not just enable subclassing by default? Well, the C implementation needs to make a special case for when an instance of a subclass is created, rather than an instance of the class itself. In
float_new
this looks like this:if (type != &PyFloat_Type) return float_subtype_new(type, args, kwds); /* Wimp out */
In this case the subclass handling is really simple, but that is because
float
on the C side is mostly a struct containing adouble
. (And if you checkslice_new
, there is no speical case for subclasses there.)As for the subclass checking, the equivalent of
isinstance
is implemented for each type by macros like these:#define PyFloat_Check(op) PyObject_TypeCheck(op, &PyFloat_Type) #define PyFloat_CheckExact(op) (Py_TYPE(op) == &PyFloat_Type)
And
PyObject_TypeCheck
is also a macro, defined as:#define PyObject_TypeCheck(ob, tp) \ (Py_TYPE(ob) == (tp) || PyType_IsSubtype(Py_TYPE(ob), (tp)))
And
PyType_IsSubtype
basically returnstp in type(ob).__mro__
, which obviously does not call any of__instancecheck__
,__subclasscheck__
and__subclasshook__
.And even if
ui
's functions accepted things that didn't actually subclassui.View
, it would probably crash the app. The reason is that on the C side, the pointer to the underlyingUIView
or controller or whatever is actually a struct field, and_objc_ptr
is likely a read-only descriptor returning that field's value. So no,_objc_ptr
is not sufficient, that's only a convenience attribute forobjc_util
. -
@dgelessus Thanks. Does this mean that creating a new class inheriting from both
ObjectProxy
andView
would have unintended side-effects? I guess I'll just have to try.The third thing I can try is having
ui2
redefine everyui.View
class, overridingadd_subview
to addview.__subject__
instead ofview
, but this is probably impractical and fragile. -
No, inheriting from
ObjectProxy
andui.View
shouldn't cause any issues, though it probably won't do what you want either. When you do this and then add an instance of that subclass to another view, it will add a blank view (the proxy object itself, which inherits fromui.View
) and not the wrapped view.The most reliable way to handle this might be to implement your wrappers as simple
ui.View
subclasses that contain the wrapped view as their only subview, with the appropriate flexing etc. set. That way the wrappers could be used anywhere a normalui.View
works. -
Hmm, you're right. It doesn't work :( I really don't like your approach, because it's easily inspectable and easy for users to break. I think I'm going to try overriding
add_subview
. @mikael has been usingadd_subview(view.__subject__)
, I'm going to try to overrideadd_subview
to try this automatically. -
Some progress: I've got a wrapper that automatically adds the wrapped as a subview.
The way Python works in this respect is really cool, imo.
>>> a = ViewProxy(ui.Slider()) >>> a <_ui.Slider object at 0x10e642410> >>> a.value 0.0 >>> a.subviews () >>> object.__getattribute__(a, "subviews") (<_ui.Slider object at 0x10e642410>,)
The reason I'm now willing to use a subview is that when doing it with a proxy it's almost completely transparent.
-
Does this let you subclass slider, as was your original goal?
-
Success, I've got a wrapper for subclassing. It displays correctly in view heirarchies.
I could be lazy and make the wrapper view have a frame like
(0, 0, 3000, 3000)
, since that's larger than any iOS device. I'll try anAutoresizingMask
though.Because of
__getattribute__
, the attributes of the container are effectively invisible. The only way isobject.__getattribute__
as displayed above. So nobody will mess with my container.It displays correctly :D
This working approach is similar to Approach 1 listed above, but my assumption that it wouldn't act as a view was incorrect. It acts as a view, but its attributes are inaccessible. I simply add an instance of the wrapped class as a subview.
-
@JonB Yes
-
It seems to me for a ViewProxy there are some attributes which should not be proxied: namely anything that is tied up and out to ObjC geometry, or a relating views to each other. None of the following should be proxied, i think, but handled by the container : name, frame, x, y, center, bounds, flex, hidden, superview, left_button_items, right_button_items, navigation_view, bring_to_front(), close(), present(), send_to_back(), wait_modal(), action or delegate. I am not sure about alpha (not sure if a None bg color lets subviews see through to the underlying ui). add_subview should also be on this list as well, since you would want a subview's superview to return the ViewProxy, not the proxied view....although most people are not adding a subview to a slider.
For positioning, I don't think a large frame is going to work well with the underlying objc code. The container frame should define the size, with the contained subview set with flex='WH' to keep the size fixed to the container.
A few properties like
border_width
or corner_radius may give undesirable behavior, unless you setclipsToBounds=False
on the subview objcinstance. Presumably you have the container bg_color set to None...Finally... likely you would want to override the
action
method of the embedded view to call the container's action with the container as sender. Otherwise ObjC will send the low level slider to the action, thus breaking the illusion of a single object. delegate methods are trickier, i suspect you would need a DelegateProxy class which intercepts all delegate calls, and calls the corresponding method in the container's delegate but replacing the first argument with the container, rather than the embedded view. -
Ugh. Thanks, though.