omz:forum

    • Register
    • Login
    • Search
    • Recent
    • Popular

    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

    Pythonista
    3
    11
    7564
    Loading More Posts
    • Oldest to Newest
    • Newest to Oldest
    • Most Votes
    Reply
    • Reply as topic
    Log in to reply
    This topic has been deleted. Only users with topic management privileges can see it.
    • Webmaster4o
      Webmaster4o last edited by Webmaster4o

      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 uninheritable ui.View classes [1] (e.g.
      TableView and Slider), 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 distinguish b from a. The only way they can be told apart is:

      >>> type(b)
      <class 'ui2.subclassing.proxies.ObjectProxy'>
      

      II. ui is uncool

      These 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 checking issubclass(type(b), ui.View). Now, there's no reason why ui couldn't have used the simpler isinstance method unless it's deliberately trying to prevent my sort of trickery. The thing is, I've read the source of ProxyTypes (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, even ObjCInstance is forwarded through the ObjectProxy:

      >>> 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.

      1. Create a new ObjectProxy subclass which uses multiple inheritance to also inherit from ui.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 an ObjectProxy subclass which uses multiple inheritance to also inherit from ui.View. I'll see if I can take __slots__ out of ProxyTypes, since I think they're just for performance.
      2. Try to write a proxy with a metaclass overriding __subclasscheck__ so that my issubclass(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 in ui2 and there's a good chance I got the licensing completely wrong.

      1 Reply Last reply Reply Quote 0
      • dgelessus
        dgelessus last edited by

        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 a struct) containing all info about the class. One of the struct fields is tp_flags, which contains various flags (duh). For example the one from PyFloat_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 has Py_TPFLAGS_BASETYPE while the one for slice does not. And if you try it in Python, you'll see that you can subclass float, but for slice 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 a double. (And if you check slice_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 returns tp 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 subclass ui.View, it would probably crash the app. The reason is that on the C side, the pointer to the underlying UIView 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 for objc_util.

        1 Reply Last reply Reply Quote 3
        • Webmaster4o
          Webmaster4o last edited by

          @dgelessus Thanks. Does this mean that creating a new class inheriting from both ObjectProxy and View would have unintended side-effects? I guess I'll just have to try.

          The third thing I can try is having ui2 redefine every ui.View class, overriding add_subview to add view.__subject__ instead of view, but this is probably impractical and fragile.

          1 Reply Last reply Reply Quote 0
          • dgelessus
            dgelessus last edited by

            No, inheriting from ObjectProxy and ui.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 from ui.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 normal ui.View works.

            1 Reply Last reply Reply Quote 0
            • Webmaster4o
              Webmaster4o last edited by

              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 using add_subview(view.__subject__), I'm going to try to override add_subview to try this automatically.

              1 Reply Last reply Reply Quote 0
              • Webmaster4o
                Webmaster4o last edited by

                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.

                1 Reply Last reply Reply Quote 0
                • JonB
                  JonB last edited by

                  Does this let you subclass slider, as was your original goal?

                  1 Reply Last reply Reply Quote 1
                  • Webmaster4o
                    Webmaster4o last edited by Webmaster4o

                    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 an AutoresizingMask though.

                    Because of __getattribute__, the attributes of the container are effectively invisible. The only way is object.__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.

                    1 Reply Last reply Reply Quote 0
                    • Webmaster4o
                      Webmaster4o last edited by

                      @JonB Yes

                      1 Reply Last reply Reply Quote 0
                      • JonB
                        JonB last edited by

                        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 set clipsToBounds=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.

                        1 Reply Last reply Reply Quote 0
                        • Webmaster4o
                          Webmaster4o last edited by

                          Ugh. Thanks, though.

                          1 Reply Last reply Reply Quote 0
                          • First post
                            Last post
                          Powered by NodeBB Forums | Contributors