Monday, June 27, 2016

Using py.test with compiled library code

Leave a Comment

I have a python library with the following repository structure:

repobase  |- mylibrary  |  |- __init__.py  |- tests     |- test_mylibrary.py 

Up to now, running the tests could simply be done by calling py.test in the repobase directory. The import mylibrary in test_mylibrary.py then used the local code in repobase/mylibrary.

Now, I've extended the library to use compiled code. Therefore the source code at repobase/mylibrary is not functional on its own. I have to do a setup.py build. This creates repobase/build/lib.linux-x86_64-2.7/mylibrary.

Is there a reasonable way to make py.test use this directory for importing mylibrary? Given these constraints:

  1. I do not want to include any sys.path / import magic in test_mylibrary.py because this may break tests in other envrionments.

  2. I don't want to give up the possibility to run py.test from repobase. Therefore modifying the PYTHONPATH does not help because . will still be first in sys.path. And thus repobase/mylibrary would be favored over repobase/build/lib.linux-x86_64-2.7/mylibrary.

If not, what's the standard way for testing python libraries, that need building?

2 Answers

Answers 1

I think your problem is simply that py.test is not copying the built shared object to the root of your repository.

I just tried running the UT straight from the Python wiki on testing C extensions using py.test as follows:

python setup.py build py.test test/examp_unittest.py 

This failed with AssertionError: No module named examp.

However, when I follow the wiki to the letter (and run python setup.py test instead), I note that it copies the .so to the root directory (note the last line before it starts running the test):

running test running egg_info writing examp.egg-info/PKG-INFO writing top-level names to examp.egg-info/top_level.txt writing dependency_links to examp.egg-info/dependency_links.txt reading manifest file 'examp.egg-info/SOURCES.txt' writing manifest file 'examp.egg-info/SOURCES.txt' running build_ext copying build/lib.linux-x86_64-2.6/examp.so -> runTest (test.examp_unittest.DeviceTest) ... ok  ---------------------------------------------------------------------- Ran 1 test in 0.001s  OK 

Having run that on my system, I can now run py.test quite happily on the same code base - as shown below.

============================= test session starts ============================== platform linux2 -- Python 2.7.3, pytest-2.9.2, py-1.4.31, pluggy-0.3.1 rootdir: /tmp/sotest, inifile:  collected 1 items   test/examp_unittest.py .  =========================== 1 passed in 0.01 seconds =========================== 

The solution is therefore to copy your shared object to the root of your repository.

To make sure I have run the whole thing from scratch, just building the extension, copying the shared object and then running py.test. This all works as expected.

Answers 2

From the discussion in chat, it sounds as if the C implementation only provides a subset of the functionality of the Python implementation.

A common solution is to split the module such that the parts which require optimized implementations exist in a separate module.

Consider a more concrete example of a library which needs convert between different image formats.

Let's assume your layout looks like this...

repobase  |- image  |  |- __init__.py  |  |- pyJPEG.py  |- build  |  |- lib.linux-x86_64-2.7  |     |- cJPEG.so  |- tests     |- test_image.py 

...your PYTHONPATH includes /path/to/repobase:/path/to/repobase/build/lib.linux-x86_64-2.7, your cJPEG.so exports symbols jpeg_decompress and jpeg_compress, and your files look like this...

image/__init__.py

# Load the C implementation if we have it, otherwise fall back to # a pure Python implementation try:     from cJPEG import jpeg_decompress, jpeg_compress except ImportError:     from pyJPEG import jpeg_decompress, jpeg_compress  def load_image(filename):     data = open(filename, 'rb').read()     if filename.endswidth('.jpg'):         return jpeg_decompress(data)     else:         raise NotImplementedError  def save_image(data, filename, filetype='JPEG'):     if filetype == 'JPEG':         data = jpeg_compress(data)     else:         raise NotImplementedError     open(filename, 'wb').write(data) 

image/pyJPEG.py

def jpeg_decompress(data):     # A pure Python implementation of a JPEG decoder  def jpeg_compress(data):     # A pure Python implementation of a JPEG encoder 

With this sort of layout, the test suite need not care whether the library is built or not - you can use the same suite in both cases, and the presence (or absence) of cJPEG.so will determine which version is tested.

If You Enjoyed This, Take 5 Seconds To Share It

0 comments:

Post a Comment