Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to test functions cdef'd in Cython?

I have a .pyx file in which I define some functions, e.g.

cdef double foo(double a) nogil:
    return 3. * a

How could I unit test the behavior of such functions outside the pyx file? Since they are cdef'd, I am not able to simply import them...

like image 946
P. Camilleri Avatar asked Feb 15 '17 20:02

P. Camilleri


People also ask

What is Cdef in Cython?

Variable and Type Definitions The cdef statement is used to declare C variables, either local or module-level: cdef int i, j, k cdef float f, g[42], *h. In C, types can be given names using the typedef statement. The equivalent in Cython is ctypedef : ctypedef int * intPtr.

What does Cdef mean in Python?

cdef declares function in the layer of C language. As you know (or not?) in C language you have to define type of returning value for each function. Sometimes function returns with void , and this is equal for just return in Python. Python is an object-oriented language.

Is Cython object-oriented?

Cython is fast at the same time provides flexibility of being object-oriented, functional, and dynamic programming language. One of the key aspects of Cython include optional static type declarations which comes out of the box.


1 Answers

To test cdef-fuctionality you need to write your tests in Cython. One could try to use cpdef-functions, however not all signatures can be used in this case (for example signatures using pointers like int *, float * and so on).

To access the cdef-functions you will need to "export" them via a pxd-file (the same can be done also for cdef-functions of extension types ):

#my_module.pyx:
cdef double foo(double a) nogil:
    return 3. * a

#my_module.pxd:
cdef double foo(double a) nogil

Now the functionality can be cimported and tested in a Cython-tester:

#test_my_module.pyx
cimport my_module

def test_foo():
    assert my_module.foo(2.0)==6.0
    print("test ok")

test_foo()

And now

>>> cythonize -i my_module.pyx
>>> cythonize -i test_my_module.pyx 
>>> python -c "import test_my_module"
test ok

Where to go from there depends on your testing infrastructure.


For example if you use unittest-module, then you could use pyximport to cythonize/load the test-module inspect it and convert all test cases into unittest-test cases or use unittest directly in your cython code (probably a better solution).

Here is a proof of concept for unittest:

#test_my_module.pyx
cimport my_module
import unittest

class CyTester(unittest.TestCase): 
    def test_foo(self):
        self.assertEqual(my_module.foo(2.0),6.0)

Now we only need to translate and to import it in pure python to be able to unittest it:

#test_cy.py 
import pyximport;
pyximport.install(setup_args = {"script_args" : ["--force"]},
                  language_level=3)

# now drag CyTester into the global namespace, 
# so tests can be discovered by unittest
from test_my_module import *

And now:

>>> python -m unittest test_cy.py
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

Btw, there is no need to cythonize pyx-modules explicitly - pyximport does it for us automatically.

A word of warning: pyximport caches cythonized c-files in ~/.pyxbld (or similar on other OSes) and as long as test_my_module.pyx has not changed the extension isn't rebuild, even if its depenencies where changed. This might be a problem (among others), when my_module changes and it leads to binary incompatibility (luckily python warns if this is the case).

By passing setup_args = {"script_args" : ["--force"]} we force a rebuild.

Another option is to delete the cached-files (one could use a temporary directory, for example created with tempfile.TemporaryDirectory(), via pyximport.install(build_dir=...)), which has the advantage of keeping the system clean.

The explicit language_level (what is language_level?) is needed in order to prevent warnings.


If you use a virtual environment and install you cython-package via setup.py (or a similar workflow), you need to make sure that *.pxd files are also included into installation, i.e. your setup-file needs to be augmented with:

from setuptools import setup, find_packages, Extension
# usual stuff for cython-modules here
...

kwargs = {
      # usual stuff for cython-modules here
      ...

      #ensure pxd-files:
      'package_data' : { 'my_module': ['*.pxd']},
      'include_package_data' : True,
      'zip_safe' : False  #needed because setuptools are used
}

setup(**kwargs)
like image 162
ead Avatar answered Oct 19 '22 05:10

ead