Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Import errors when running nosetests that I can't reproduce outside of nose

I'm running into a mysterious import error when using nosetests to run a test suite that I can't reproduce outside of the nose. Furthermore, the import error disappears when I skip a subset of the tests.

Executive Summary: I am getting an import error in Nose that a) only appears when tests bearing a certain attribute are excluded and b) cannot be reproduced in an interactive python session, even when I ensure that the sys.path is the same for both.

Details:

The package structure looks like this:

project/
    module1/__init__.py
    module1/foo.py
    module1/test/__init__.py
    module1/test/foo_test.py
    module1/test/test_data/foo_test_data.txt
    module2/__init__.py
    module2/bar.py
    module2/test/__init__.py
    module2/test/bar_test.py
    module2/test/test_data/bar_test_data.txt

Some of the tests in foo_test.py are slow, so I've created a @slow decorator to allow me to skip them with a nosetests option:

def slow(func):
    """Decorator sets slow attribute on a test method, so 
       nosetests can skip it in quick test mode."""
    func.slow = True
    return func

class TestFoo(unittest.TestCase):

    @slow
    def test_slow_test(self):
        load_test_data_from("test_data/")
        slow_test_operations_here


    def test_fast_test(self):
        load_test_data_from("test_data/")

When I want to run the fast unit tests only, I use

nosetests -vv -a'!slow'

from the root directory of the project. When I want to run them all, I remove the final argument.

Here comes the detail that I suspect is to blame for this mess. The unit tests need to load test data from files (not best practice, I know.) The files are placed in a directory called "test_data" in each test package, and the unit test code refers to them by a relative path, assuming the unit test is being run from the test/ directory, as shown in the example code above.

To get this to work with running nose from the root directory of the project, I added the following code to init.py in each test package:

import os
import sys

orig_wd = os.getcwd()

def setUp():
    """
    test package setup:  change working directory to the root of the test package, so that 
    relative path to test data will work.
    """
    os.chdir(os.path.dirname(os.path.abspath(__file__)))

def tearDown():
    global orig_wd
    os.chdir(orig_wd)

As far as I understand, nose executes the setUp and tearDown package methods before and after running the tests in that package, which ensures that the unit test can find the appropriate test_data directory, and the working directory is reset to the original value when the tests are complete.

So much for the setup. The problem is, I get an import error only when I run the full suite of tests. The same modules import just fine when I exclude the slow tests. (To clarify, the tests throwing import errors are not slow, so they execute in either scenario.)

$ nosetests
...

ERROR: Failure: ImportError (No module named foo_test)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Library/Python/2.7/site-packages/nose/loader.py", line 413, in loadTestsFromName
    addr.filename, addr.module)
  File "/Library/Python/2.7/site-packages/nose/importer.py", line 47, in importFromPath
    return self.importFromDir(dir_path, fqname)
  File "/Library/Python/2.7/site-packages/nose/importer.py", line 80, in importFromDir
    fh, filename, desc = find_module(part, path)
ImportError: No module named foo_test

If I run the test suite without the slow tests, then no error:

$ nosetests -a'!slow'

...

test_fast_test (module1.test.foo_test.TestFoo) ... ok

In a python interactive session, I can import the test module with no trouble:

$ python
Python 2.7.1 (r271:86832, Aug  5 2011, 03:30:24) 
[GCC 4.2.1 (Based on Apple Inc. build 5658) (LLVM build 2335.15.00)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import module1.test
>>> module1.test.__path__
['/Users/USER/project/module1/test']
>>> dir(module1.test)
['__builtins__', '__doc__', '__file__', '__name__', '__package__', '__path__', 'orig_wd', 'os', 'setUp', 'sys', 'tearDown']

When I set a breakpoint in nose/importer.py, things look different:

> /Library/Python/2.7/site-packages/nose/importer.py(83)importFromDir()
-> raise
(Pdb) l
 78                               part, part_fqname, path)
 79                     try:
 80                         fh, filename, desc = find_module(part, path)
 81                     except ImportError, e:
 82                         import pdb; pdb.set_trace()
 83  ->                     raise
 84                     old = sys.modules.get(part_fqname)
 85                     if old is not None:
 86                         # test modules frequently have name overlap; make sure
 87                         # we get a fresh copy of anything we are trying to load
 88                         # from a new path

(Pdb) part
'foo_test'
(Pdb) path
['/Users/USER/project/module1/test']
(Pdb) import module1.test.foo_test
*** ImportError: No module named foo_test
#If I import module1.test, it works, but the __init__.py file is not being executed
(Pdb) import partition.test
(Pdb) del dir
(Pdb) dir(partition.test)
['__doc__', '__file__', '__name__', '__package__', '__path__'] #setUp and tearDown missing?
(Pdb) module1.test.__path__
['/Users/USER/project/module1/test']  #Module path is the same as before.
(Pdb) os.listdir(partition.test.__path__[0])  #All files are right where they should be...
['.svn', '__init__.py', '__init__.pyc', 'foo_test.py', 'foo_test.pyc','test_data']

I see the same screwy results even if I copy sys.path from my interactive session into the pdb session and repeat the above. Can anyone give me any insight about what might be going on? I realize I'm doing several non-standard things at the same time, which could lead to strange interactions. I'd be as interested in advice on how to simplify my architecture as I would be to get an explanation for this bug.

like image 331
Alex Hasha Avatar asked Apr 24 '13 19:04

Alex Hasha


3 Answers

Here is how to track down the context of the error.

nosetests --debug=nose,nose.importer --debug-log=nose_debug <your usual args>

Afterwards, check the nose_debug file. Search for your error message "No module named foo_test". Then look at the preceding few lines to see which files/directories nose was looking at.

In my case, nose was attempting to run some code which I had imported into my codebase - a 3rd party module which contained its own tests, but which I was not intending to include in my test suite. To resolve this, I used the nose-exclude plugin to exclude this directory.

like image 137
jdhildeb Avatar answered Nov 15 '22 14:11

jdhildeb


It's just nose adjusting your path by default. It will change sys.path before importing your module, possibly allowing double code execution and imports outside of package (like your case).

To avoid this, setup your PYTHONPATH before running nose and use nose --no-path-adjustment. See: http://nose.readthedocs.org/en/latest/usage.html#cmdoption--no-path-adjustment

If you cannot add a command line argument you can use an env var (NOSE_NOPATH=y) or this in .noserc:

[nosetests]
no-path-adjustment=1
like image 25
ionelmc Avatar answered Nov 15 '22 14:11

ionelmc


I encountered this problem, and traced it to 1) forgetting to activate the virtualenv I was using, and 2) my shell, zsh, apparently having cached the path to the wrong instance of the nosetests executable on my machine.

Once I activated my virtualenv, then gave the shell command hash -r, this error stopped occurring. Sorry, I didn't pin down whether only one of those would have been sufficient.

I found this reply by raffienficiaud, to nose issue "nosetest does not honour virtual environments", helpful:

For the record, it is a bash issue that caches commands. In that case, which nosetests points (deterministically) to the right executable, while bash cached the system installed one. Using hash -r clears the cache (see http://unix.stackexchange.com/questions/5609/how-do-i-clear-bashs-cache-of-paths-to-executables)

That Unix.SE answer is to a question, "How do I clear Bash's cache of paths to executables?", by Tobu and Zigg.

bash does cache the full path to a command. You can verify that the command you are trying to execute is hashed with the type command:

$ type svnsync svnsync is hashed (/usr/local/bin/svnsync)

To clear the entire cache:

$ hash -r

Or just one entry:

$ hash -d svnsync

For additional information, consult help hash and man bash.

I use zsh not bash, and hash -d nosetests gave me an error message. Nevertheless, the problem was gone after I did hash -r.

like image 44
Jim DeLaHunt Avatar answered Nov 15 '22 13:11

Jim DeLaHunt