Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

nose-doctest module fixture before module is imported

I use nose for test collection and I also want to use its doctest plugin. I have a module that needs a fixture in order to be importable. Therefore, I cannot use nose's module fixtures, since they are loaded from the module under test. Is there a way to specify module fixtures for nose-doctest outside of the module?

For some use-cases, an option would be to detect being run under doctest and to apply the fixture at the beginning of the module. I'd be interested to hear answers for this use-case as-well.

However, there are situations where this cannot work: When the import fails due to a SyntaxError, no module code is ever run. In my case, I'm mostly developing code that is both compatible with python 2 and python 3 (without 2to3). There are a few python 3 specific modules however, which simply should not be inspected at all by nose, when run under python 2. What would my best option be here?

EDIT: MWE (for the SyntaxError situation)

I have a package with many small modules, some of them use python 3 syntax. Here's the package structure:

~/pckg/
  __init__.py
  py3only.py
  ... (other modules)
  tests/
    test_py3only.py

Some tests are written as unittest.TestCase, but I also want the code examples in the docstrings tested. ~/pckg/__init__.py is empty.

~/pckg/py3only.py:

def fancy_py3_func(a:"A function argument annotation (python 3 only syntax)"):
    """ A function using fancy syntax doubling it's input.

    >>> fancy_py3_func(4)
    8
    """
    return a*2

~/pckg/tests/test_py3only.py:

import sys, unittest

def setup_module():
    if sys.version_info[0] < 3:
        raise unittest.SkipTest("py3only unavailable on python "+sys.version)

class TestFancyFunc(unittest.TestCase):
    def test_bruteforce(self):
        from pckg.py3only import fancy_py3_func
        for k in range(10):
            self.assertEqual(fancy_py3_func(k),2*k)

Testing on python 3, everything gets tested and passes (run from the enclosing folder, e.g. ~):

~ nosetests3 -v --with-doctest pckg
Doctest: pckg.py3only.fancy_py3_func ... ok
test_bruteforce (test_py3only.TestFancyFunc) ... ok

On python 2, the module fixture of ~/pckg/tests/test_py2only.py properly detects the situation and skips the test. However, we get a SyntaxError from ~/pckg/py3only.py:

~ nosetests -v --with-doctest pckg 
Failure: SyntaxError (invalid syntax (py3only.py, line 1)) ... ERROR
SKIP: py3only unavailable on python 2.7.6 (default, Mar 22 2014, 22:59:56)

A function similar to ~/pckg/tests/test_py3only.py:setup_module() could solve that problem, if I could get nose to run that code before it's doctest plugin even attempts to import that module.

It looks like my best bet is to write a proper top-level test script that handles the collection of the tests...

like image 235
burnpanck Avatar asked Oct 21 '22 00:10

burnpanck


1 Answers

Specific test files, directories, classes or methods can be excluded using nose-exclude nose plugin. It has --exclude-* options.

To handle missing modules, you have to patch sys.modules using mock.

F.e, There's a Calc class in mycalc module, but I don't have access to it because it's missing. And there are two more modules, mysuper_calc and mysuper_calc3, the latter is Python 3 specific. These two modules import mycalc and mysuper_calc3 shouldn't be tested under Python 2. How to doctest them out of module, that's in plain text file? I assume this is OP's situation.

calc/mysuper_calc3.py

from sys import version_info
if version_info[0] != 3:
    raise Exception('Python 3 required')
from mycalc import Calc
class SuperCalc(Calc):
    '''This class implements an enhanced calculator
    '''
    def __init__(self):
        Calc.__init__(self)

    def add(self, n, m):
        return Calc.add(self, n, m)

calc/mysuper_calc.py

from mycalc import Calc

class SuperCalc(Calc):
    '''This class implements an enhanced calculator
    '''
    def __init__(self):
        Calc.__init__(self)

    def add(self, n, m):
        return Calc.add(self, n, m)

Now to mock out mycalc,

>>> from mock import Mock, patch
>>> mock = Mock(name='mycalc')

Module mycalc has class Calc which has method add. I test SuperCalc instance add method with 2+3.

>>> mock.Calc.add.return_value = 5  

Now patch sys.modules and mysuper_calc3 can be conditionally imported within the with block.

>>> with patch.dict('sys.modules',{'mycalc': mock}):
...     from mysuper_calc import SuperCalc
...     if version_info[0] == 3:
...         from mysuper_calc3 import SuperCalc

calc/doctest/mysuper_calc_doctest.txt

>>> from sys import version_info
>>> from mock import Mock, patch
>>> mock = Mock(name='mycalc')
>>> mock.Calc.add.return_value = 5

>>> with patch.dict('sys.modules',{'mycalc': mock}):
...     from mysuper_calc import SuperCalc
...     if version_info[0] == 3:
...         from mysuper_calc3 import SuperCalc
>>> c = SuperCalc()
>>> c.add(2,3)
5

The file mysuper_calc_doctest.txt has to be alone in its own directory otherwise nosetests searches for doctest in non-test modules.

PYTHONPATH=.. nosetests --with-doctest --doctest-extension=txt --verbosity=3

Doctest: mysuper_calc_doctest.txt ... ok


Ran 1 test in 0.038s

OK

A wrapper around nosetests to detect Python 3 which passes .py files without syntax errors to nosetests

mynosetests.py

import sys
from subprocess import Popen, PIPE
from glob import glob

f_list = []

py_files = glob('*py')
try:
    py_files.remove(sys.argv[0])
except ValueError:
    pass

for py_file in py_files:
    try:
        exec open(py_file)
    except SyntaxError:
        continue
    else:
        f_list.append(py_file)

proc = Popen(['nosetests'] + sys.argv[1:] + f_list,stdout=PIPE, stderr=PIPE)
print('%s\n%s' % proc.communicate())
sys.exit(proc.returncode)
like image 98
Nizam Mohamed Avatar answered Nov 03 '22 00:11

Nizam Mohamed