1

I am messing with Cython, diving deeper into kivy and I have been trying my hand at making my own Kivy Property.

I have the following files difining a DocProperty: my.pyx:

from kivy.properties cimport Property, PropertyStorage
from kivy._event cimport EventDispatcher

cdef inline void observable_object_dispatch(object self, str name):
    cdef Property prop = self.prop
    prop.dispatch(self.obj, name)


class ObservableObject(object):

    # Internal class to observe changes inside a native python object.
    def __init__(self, *largs):
        self.prop = largs[0]
        self.obj = largs[1]
        super(ObservableObject, self).__init__()

    def __setattr__(self, name, value):
        object.__setattr__(self, name, value)
        observable_object_dispatch(self, name)


cdef class DocProperty(Property):

    def __init__(self, defaultvalue=None, rebind=False, **kw):
        self.baseclass = kw.get('baseclass', object)
        super(DocProperty, self).__init__(defaultvalue, **kw)
        self.rebind = rebind

    cpdef link(self, EventDispatcher obj, str name):
        Property.link(self, obj, name)
        cdef PropertyStorage ps = obj.__storage[self._name]
        ps.value = ObservableObject(self, obj, ps.value)

    cdef check(self, EventDispatcher obj, value):
        if Property.check(self, obj, value):
            return True
        if not isinstance(value, object):
            raise ValueError('{}.{} accept only object based on {}'.format(
            obj.__class__.__name__,
            self.name,
            self.baseclass.__name__))

    cpdef dispatch(self, EventDispatcher obj, str name):
        '''Dispatch the value change to all observers.
       .. versionchanged:: 1.1.0
           The method is now accessible from Python.
       This can be used to force the dispatch of the property, even if the
       value didn't change::
           button = Button()
           # get the Property class instance
           prop = button.property('text')
           # dispatch this property on the button instance
           prop.dispatch(button)
       '''
        cdef PropertyStorage ps = obj.__storage[self._name]
        ps.observers.dispatch(obj, ps.value, (name,), None, 0)

my.pxd:

from kivy.properties cimport Property, PropertyStorage
from kivy._event cimport EventDispatcher

cdef class DocProperty(Property):
    cdef object baseclass
    cdef public int rebind
    cpdef dispatch(self, EventDispatcher obj, str name)

And a quick sript to try it out: my.py

# -*- coding: utf-8 -*-

from kivy.event import EventDispatcher
import pyximport
pyximport.install()
from properties import DocProperty

if __name__ == '__main__':

    class ED(EventDispatcher):

        doc = DocProperty()

        def on_doc(self, obj, value):
            print 'printing doc', self.doc

    class DumbObj(object):

        def __init__(self, num):
            self._num = num

        @property
        def num(self):
            return 5

        @num.setter
        def num(self, value):
            self._num = value

    ed = ED()
    ed.doc = DumbObj(3)
    ed.doc.num = 4

When I run my.py, I get a 'Signature not compatible with previous declaration' on the dispatch method of DocProperty because I try override its declaration over at Property so it can accept one argument more than the original code declaration. Is it even possible to overload cpdef methods declared on pxd? If so, what am I doing wrong?

Edit: Following @ead's suggestion, I tried replacing cpdef statements with plain def on the declarations of dispatch, on both files and only one of them at a time. But that had no effect. I then tried to comment out the the call to dispatch to see what goes on if it didn't fail to compile. Turns out both attributes of DocProperty (baseclass and bind) raise AttributeError on assignment. Which is weird, because those were copy/pasted from Kivy source. This means my.pxd file is not having any effect on my Cython code? I tried from cimporting my.pxd into my.pyx, but that yielded not result also

Guga Figueiredo
  • 292
  • 4
  • 18

1 Answers1

0

I don't know why you would like do something like this and I'm not sure it is a wise thing to do in the long run. But this question is not without (at least theoretical) interest.

There is method overriding - same name, same signature, different implementation - and this is possible in Cython, for example:

%%cython
cdef class A:
  cpdef get_number(self):
    return 0

cdef class B(A):
  cpdef get_number(self):#overrides method
    return 1

def print_number(A obj):
    print("My number is", obj.get_number()) #uses C-level dispatch when possible

and now:

>>> print_number(A())
0
>>> print_number(B())
1

There is more to these code-lines, than meets the eye: the dispatch happens not in Python but on the C-level - much faster but somewhat less flexible than Python's dispatch.

There is also function overloading - same name, different signatures, both methods can be used - this is not really possible in Python and thus also not in Cython. However, unlike in C, Python allows to change the signature of the method with the same name, so that the last signature is used:

>>> class SomeClass:
     def do_something(self):
       pass
     def do_something(self, i):
       pass
>>> SomeClass().do_something(7) #OK
>>> SomeClass().do_something() #Error, argument is needed

You can achieve the same behavior with Cython, using def instead of cpdef functions (but the function should no longer be declared in pxd though you can leave it as cpdef in the parent classes):

%%cython
...
class C(B):
  def get_number(self, m):
    return min(42,m)

leads now to an error:

>>> print_number(C())
TypeError: get_number() missing 1 required positional argument: 'm'

This time Python-dispatch is used (here is a little more information on how it works) and "last" definition of get_number in the class-hierarchy expects an parameter which is not provided - thus the error.


One more thing: Why is it not possible to change the signature of function and leave it as cpdef?

cpdef functions have a statically typed part. Class A and all its subclasses have in their function-table a pointer to the implementation of get_number(self) and this pointer is of type:

PyObject *(*get_number)(struct __pyx_obj_4test_A *, int __pyx_skip_dispatch);

for example in the table for A:

struct __pyx_vtabstruct_4test_A {
  PyObject *(*get_number)(struct __pyx_obj_4test_A *, int __pyx_skip_dispatch);
};

Type corresponding to get_number(self, int m) would be:

PyObject *(*get_number)(struct __pyx_obj_4test_B *, int, int __pyx_skip_dispatch);

There is an additional int in the signature, thus it is of different type and cannot be written to __pyx_vtabstruct_4test_A.get_number - here the Cython behaves as C.

On the other hand you cannot have two different members in a struct with the same name, thus the real overloading (as for example in C++ with its namemangling) is not possible.

ead
  • 32,758
  • 6
  • 90
  • 153
  • If I understand correctly, it IS possible to do what I want (overloading the method, not overriding ,thanks for pointing it out). So I tried using `def dispatch()` declaration on my.pyx and my.pxd files, but problem persisted, I still got `"Error compiling ... Call with wrong number of arguments (expected 2, got 3)"`. So I tried removing the line calling `dispatch()` to see what happens and found that my.pxd seems to have no effect on the my.pyx file, as if the declarations there are not being imported. I will edit my question on include details – Guga Figueiredo Jun 18 '18 at 22:14
  • @GugaFigueiredo There is no real overloading in Python. You can change the signature of a method, but cannot use the old one. So if it is real overloading you are after, then you cannot do it. – ead Jun 19 '18 at 04:03
  • @GugaFigueiredo In pxd you put only cdef and cpdef so if function becomes def it should no longer be in pxd. 3) "but problem persisted" is rather "another problem occurred", which is not reproducible with your current example. – ead Jun 19 '18 at 04:03