Repository: numba/numba

Issue:
NUMBA_DISABLE_JIT=1 breaks code with jitclass
Using Numba 0.24.0, the following code produces an error (see below) when setting `NUMBA_DISABLE_JIT=1`:

```
import numpy as np
import numba as nb


spec = [
    ('h', nb.float64[:,::1]),
    ('x', nb.float64[:,::1])
    ]

@nb.jitclass(spec)
class Data(object):
    def __init__(self, N):
        self.h = np.zeros((N,N), dtype=np.float64)
        self.x = np.zeros((N,N), dtype=np.float64)

if __name__ == '__main__':
    data = Data(3)
    print data.h
```

With error traceback:

``` python
export NUMBA_DISABLE_JIT=1 && python test.py
Traceback (most recent call last):
  File "test.py", line 17, in <module>
    data = Data(3)
  File "/Users/lev/anaconda/lib/python2.7/site-packages/numba/jitclass/base.py", line 94, in __call__
    return cls._ctor(*args, **kwargs)
  File "/Users/lev/anaconda/lib/python2.7/site-packages/numba/decorators.py", line 31, in __call__
    return self.py_func(*args, **kwargs)
  File "<string>", line 3, in ctor
  File "/Users/lev/anaconda/lib/python2.7/site-packages/numba/jitclass/base.py", line 94, in __call__
    return cls._ctor(*args, **kwargs)
  File "/Users/lev/anaconda/lib/python2.7/site-packages/numba/decorators.py", line 31, in __call__
    return self.py_func(*args, **kwargs)
  File "<string>", line 3, in ctor

  ...
  File "<string>", line 3, in ctor
  File "/Users/lev/anaconda/lib/python2.7/site-packages/numba/jitclass/base.py", line 94, in __call__
    return cls._ctor(*args, **kwargs)
  File "/Users/lev/anaconda/lib/python2.7/site-packages/numba/decorators.py", line 31, in __call__
    return self.py_func(*args, **kwargs)
  File "<string>", line 3, in ctor
  File "/Users/lev/anaconda/lib/python2.7/site-packages/numba/jitclass/base.py", line 94, in __call__
    return cls._ctor(*args, **kwargs)
  File "/Users/lev/anaconda/lib/python2.7/site-packages/numba/decorators.py", line 31, in __call__
    return self.py_func(*args, **kwargs)
RuntimeError: maximum recursion depth exceeded
```



Discussion:

Resolving patch:
diff --git a/numba/jitclass/decorators.py b/numba/jitclass/decorators.py
--- a/numba/jitclass/decorators.py
+++ b/numba/jitclass/decorators.py
@@ -1,5 +1,6 @@
 from __future__ import absolute_import, print_function
-from numba import types
+
+from numba import config, types
 from .base import register_class_type, ClassBuilder
 
 
@@ -20,6 +21,9 @@ def jitclass(spec):
     A callable that takes a class object, which will be compiled.
     """
     def wrap(cls):
-        return register_class_type(cls, spec, types.ClassType, ClassBuilder)
+        if config.DISABLE_JIT:
+            return cls
+        else:
+            return register_class_type(cls, spec, types.ClassType, ClassBuilder)
 
     return wrap


Test patch:
diff --git a/numba/tests/support.py b/numba/tests/support.py
--- a/numba/tests/support.py
+++ b/numba/tests/support.py
@@ -568,3 +568,36 @@ def setUp(self):
     def tearDown(self):
         super(MemoryLeakMixin, self).tearDown()
         self.memory_leak_teardown()
+
+
+@contextlib.contextmanager
+def forbid_codegen():
+    """
+    Forbid LLVM code generation during the execution of the context
+    manager's enclosed block.
+
+    If code generation is invoked, a RuntimeError is raised.
+    """
+    from numba.targets import codegen
+    patchpoints = ['CodeLibrary._finalize_final_module']
+
+    old = {}
+    def fail(*args, **kwargs):
+        raise RuntimeError("codegen forbidden by test case")
+    try:
+        # XXX use the mock library instead?
+        for name in patchpoints:
+            parts = name.split('.')
+            obj = codegen
+            for attrname in parts[:-1]:
+                obj = getattr(obj, attrname)
+            attrname = parts[-1]
+            value = getattr(obj, attrname)
+            assert callable(value), ("%r should be callable" % name)
+            old[obj, attrname] = value
+            setattr(obj, attrname, fail)
+        yield
+    finally:
+        for (obj, attrname), value in old.items():
+            setattr(obj, attrname, value)
+
diff --git a/numba/tests/test_debug.py b/numba/tests/test_debug.py
--- a/numba/tests/test_debug.py
+++ b/numba/tests/test_debug.py
@@ -4,9 +4,9 @@
 import platform
 import textwrap
 
-from .support import TestCase, override_config, captured_stdout
+from .support import TestCase, override_config, captured_stdout, forbid_codegen
 from numba import unittest_support as unittest
-from numba import types
+from numba import jit, jitclass, types
 from numba.compiler import compile_isolated
 
 
@@ -19,6 +19,16 @@ def simple_gen(x, y):
     yield y
 
 
+class SimpleClass(object):
+    def __init__(self):
+        self.h = 5
+
+simple_class_spec = [('h', types.int32)]
+
+def simple_class_user(obj):
+    return obj.h
+
+
 class DebugTestBase(TestCase):
 
     all_dumps = set(['bytecode', 'cfg', 'ir', 'typeinfer', 'llvm',
@@ -144,6 +154,29 @@ def test_dump_ir_generator(self):
         self.assertIn(expected_gen_info, out)
 
 
+class TestDisableJIT(DebugTestBase):
+    """
+    Test the NUMBA_DISABLE_JIT environment variable.
+    """
+
+    def test_jit(self):
+        with override_config('DISABLE_JIT', True):
+            with forbid_codegen():
+                cfunc = jit(nopython=True)(simple_nopython)
+                self.assertPreciseEqual(cfunc(2), 3)
+
+    def test_jitclass(self):
+        with override_config('DISABLE_JIT', True):
+            with forbid_codegen():
+                SimpleJITClass = jitclass(simple_class_spec)(SimpleClass)
+
+                obj = SimpleJITClass()
+                self.assertPreciseEqual(obj.h, 5)
+
+                cfunc = jit(nopython=True)(simple_class_user)
+                self.assertPreciseEqual(cfunc(obj), 5)
+
+
 class TestEnvironmentOverride(FunctionDebugTestBase):
     """
     Test that environment variables are reloaded by Numba when modified.
diff --git a/numba/tests/test_support.py b/numba/tests/test_support.py
--- a/numba/tests/test_support.py
+++ b/numba/tests/test_support.py
@@ -4,9 +4,9 @@
 
 import numpy as np
 
-from numba import utils
+from numba import jit, utils
 from numba import unittest_support as unittest
-from .support import TestCase
+from .support import TestCase, forbid_codegen
 
 DBL_EPSILON = 2**-52
 FLT_EPSILON = 2**-23
@@ -312,6 +312,19 @@ def test_assertRefCount(self):
                 l.append(y)
         self.assertIn("66", str(cm.exception))
 
+    def test_forbid_codegen(self):
+        """
+        Test that forbid_codegen() prevents code generation using the @jit
+        decorator.
+        """
+        def f():
+            return 1
+        with forbid_codegen():
+            with self.assertRaises(RuntimeError) as raises:
+                cfunc = jit(nopython=True)(f)
+                cfunc()
+        self.assertIn("codegen forbidden by test case", str(raises.exception))
+
 
 if __name__ == '__main__':
     unittest.main()