Repository: pandas-dev/pandas

Issue:
version dependency/tests on numexpr -- truediv etc are NA in numexpr 1.4.2
while building for Ubuntu 12.04, bulk of tests fail due e.g.
`ValueError: Unknown keyword argument 'truediv'` from numexpr and `AssertionError: Did not use numexpr as expected` in test_expressions

Full log http://neuro.debian.net/_files/_buildlogs/pandas/0.13.0/pandas_0.13.0-2~nd12.04+1_i386.build



Discussion:
I don't think ever run on < Numexpr 2
which has been out since 2011

shouldn't this error have shown up before?

want me to put up a PR that:
1. Checks >= 2.0 for numexpr
2. Sets can use numexpr setting to False if it's < 2.0
3. Skips tests (probably in setup) in test_expressions

?

would be gr8 @jtratner

might be worthwhile so we could provide builds for elderly ubuntus, may be without the benefits of recent numexpr.  Debian wheezy has numexpr 2.0.1

maybe also change one of the Travis builds (maybe 2.6 one

to use Numexpr 1.4.2

@yarikoptic can you try building this branch and seeing if it works for you - I just added the division future import to all core files and removed the truediv kwarg from expressions. I believe this will actually make numexpr 1.4.2 work:

https://github.com/jtratner/pandas/tree/handle-old-numexpr

On Fri, 03 Jan 2014, Jeff Tratner wrote:

>    [1]@yarikoptic can you try building this branch and seeing if it works for
>    you - I just added the division future import to all core files and
>    removed the truediv kwarg from expressions. I believe this will actually
>    make numexpr 1.4.2 work:
> 
>    [2]https://github.com/jtratner/pandas/tree/handle-old-numexpr

Hi Jeff,

Either I screwed up somewhere (I always leave that possibility even to obvious
actions), or they had no complete healing effect (I think one of the
failles is gone), I still observe the same failures after cherry-picking

0001-Add-division-future-import-everywhere.patch
0002-remove-explicit-truediv-kwarg.patch

e.g. building on ubuntu 12.04 with numexpr 1.4.2-1.2build2

# 

## ERROR: pandas.computation.tests.test_eval.TestAlignment.test_align_nested_unary_op('numexpr', 'python')

Traceback (most recent call last):
  File "/usr/lib/python2.7/dist-packages/nose/case.py", line 197, in runTest
    self.test(*self.arg)
  File "/tmp/buildd/pandas-0.13.0/debian/tmp/usr/lib/python2.7/dist-packages/pandas/computation/tests/test_eval.py", line 794, in check_align_nested_unary_op
    res = pd.eval(s, engine=engine, parser=parser)
  File "/tmp/buildd/pandas-0.13.0/debian/tmp/usr/lib/python2.7/dist-packages/pandas/computation/eval.py", line 209, in eval
    ret = eng_inst.evaluate()
  File "/tmp/buildd/pandas-0.13.0/debian/tmp/usr/lib/python2.7/dist-packages/pandas/computation/engines.py", line 52, in evaluate
    res = self._evaluate()
  File "/tmp/buildd/pandas-0.13.0/debian/tmp/usr/lib/python2.7/dist-packages/pandas/computation/engines.py", line 100, in _evaluate
    truediv=self.expr.truediv)
  File "/usr/lib/pymodules/python2.7/numexpr/necompiler.py", line 624, in evaluate
    context = getContext(kwargs)
  File "/usr/lib/pymodules/python2.7/numexpr/necompiler.py", line 436, in getContext
    raise ValueError("Unknown keyword argument '%s'" % map.popitem()[0])
ValueError: Unknown keyword argument 'truediv'

## 

Yaroslav O. Halchenko, Ph.D.
http://neuro.debian.net http://www.pymvpa.org http://www.fail2ban.org
Senior Research Associate,     Psychological and Brain Sciences Dept.
Dartmouth College, 419 Moore Hall, Hinman Box 6207, Hanover, NH 03755
Phone: +1 (603) 646-9834                       Fax: +1 (603) 646-1419
WWW:   http://www.linkedin.com/in/yarik

That's very strange. It shouldn't be adding truediv kwarg anywhere and I
ran the suite locally against numexpr 1.4.2. I will run again and try to
figure out what's up.

That said, I realize we may not be ablr support numexpr 1.4.2 because it
will segfault on certain division edge cases. This was only fixed a few
months ago...

I need to check this and the resolution may be to force no acceleration
under a specific version number of numexpr


Resolving patch:
diff --git a/pandas/computation/eval.py b/pandas/computation/eval.py
--- a/pandas/computation/eval.py
+++ b/pandas/computation/eval.py
@@ -3,13 +3,11 @@
 """Top level ``eval`` module.
 """
 
-import numbers
-import numpy as np
 
 from pandas.core import common as com
-from pandas.compat import string_types
 from pandas.computation.expr import Expr, _parsers, _ensure_scope
 from pandas.computation.engines import _engines
+from distutils.version import LooseVersion
 
 
 def _check_engine(engine):
@@ -38,7 +36,13 @@ def _check_engine(engine):
             import numexpr
         except ImportError:
             raise ImportError("'numexpr' not found. Cannot use "
-                              "engine='numexpr' if 'numexpr' is not installed")
+                              "engine='numexpr' for query/eval "
+                              "if 'numexpr' is not installed")
+        else:
+            ne_version = numexpr.__version__
+            if ne_version < LooseVersion('2.0'):
+                raise ImportError("'numexpr' version is %s, "
+                                  "must be >= 2.0" % ne_version)
 
 
 def _check_parser(parser):
diff --git a/pandas/computation/expressions.py b/pandas/computation/expressions.py
--- a/pandas/computation/expressions.py
+++ b/pandas/computation/expressions.py
@@ -8,10 +8,11 @@
 
 import numpy as np
 from pandas.core.common import _values_from_object
+from distutils.version import LooseVersion
 
 try:
     import numexpr as ne
-    _NUMEXPR_INSTALLED = True
+    _NUMEXPR_INSTALLED = ne.__version__ >= LooseVersion('2.0')
 except ImportError:  # pragma: no cover
     _NUMEXPR_INSTALLED = False
 


Test patch:
diff --git a/pandas/computation/tests/test_eval.py b/pandas/computation/tests/test_eval.py
--- a/pandas/computation/tests/test_eval.py
+++ b/pandas/computation/tests/test_eval.py
@@ -2,6 +2,7 @@
 
 import functools
 from itertools import product
+from distutils.version import LooseVersion
 
 import nose
 from nose.tools import assert_raises, assert_true, assert_false, assert_equal
@@ -17,13 +18,13 @@
 from pandas.util.testing import makeCustomDataframe as mkdf
 
 from pandas.computation import pytables
-from pandas.computation.expressions import _USE_NUMEXPR
 from pandas.computation.engines import _engines
 from pandas.computation.expr import PythonExprVisitor, PandasExprVisitor
-from pandas.computation.ops import (_binary_ops_dict, _unary_ops_dict,
+from pandas.computation.ops import (_binary_ops_dict,
                                     _special_case_arith_ops_syms,
                                     _arith_ops_syms, _bool_ops_syms)
 from pandas.computation.common import NameResolutionError
+
 import pandas.computation.expr as expr
 import pandas.util.testing as tm
 from pandas.util.testing import (assert_frame_equal, randbool,
@@ -35,11 +36,6 @@
 _scalar_skip = 'in', 'not in'
 
 
-def skip_if_no_ne(engine='numexpr'):
-    if not _USE_NUMEXPR and engine == 'numexpr':
-        raise nose.SkipTest("numexpr engine not installed or disabled")
-
-
 def engine_has_neg_frac(engine):
     return _engines[engine].has_neg_frac
 
@@ -108,7 +104,7 @@ class TestEvalNumexprPandas(tm.TestCase):
     @classmethod
     def setUpClass(cls):
         super(TestEvalNumexprPandas, cls).setUpClass()
-        skip_if_no_ne()
+        tm.skip_if_no_ne()
         import numexpr as ne
         cls.ne = ne
         cls.engine = 'numexpr'
@@ -426,7 +422,7 @@ def check_single_invert_op(self, lhs, cmp1, rhs):
             assert_array_equal(expected, result)
 
             for engine in self.current_engines:
-                skip_if_no_ne(engine)
+                tm.skip_if_no_ne(engine)
                 assert_array_equal(result, pd.eval('~elb', engine=engine,
                                                    parser=self.parser))
 
@@ -457,7 +453,7 @@ def check_compound_invert_op(self, lhs, cmp1, rhs):
 
             # make sure the other engines work the same as this one
             for engine in self.current_engines:
-                skip_if_no_ne(engine)
+                tm.skip_if_no_ne(engine)
                 ev = pd.eval(ex, engine=self.engine, parser=self.parser)
                 assert_array_equal(ev, result)
 
@@ -709,7 +705,7 @@ class TestEvalNumexprPython(TestEvalNumexprPandas):
     @classmethod
     def setUpClass(cls):
         super(TestEvalNumexprPython, cls).setUpClass()
-        skip_if_no_ne()
+        tm.skip_if_no_ne()
         import numexpr as ne
         cls.ne = ne
         cls.engine = 'numexpr'
@@ -788,7 +784,7 @@ class TestAlignment(object):
     lhs_index_types = index_types + ('s',)  # 'p'
 
     def check_align_nested_unary_op(self, engine, parser):
-        skip_if_no_ne(engine)
+        tm.skip_if_no_ne(engine)
         s = 'df * ~2'
         df = mkdf(5, 3, data_gen_f=f)
         res = pd.eval(s, engine=engine, parser=parser)
@@ -799,7 +795,7 @@ def test_align_nested_unary_op(self):
             yield self.check_align_nested_unary_op, engine, parser
 
     def check_basic_frame_alignment(self, engine, parser):
-        skip_if_no_ne(engine)
+        tm.skip_if_no_ne(engine)
         args = product(self.lhs_index_types, self.index_types,
                        self.index_types)
         for lr_idx_type, rr_idx_type, c_idx_type in args:
@@ -815,7 +811,7 @@ def test_basic_frame_alignment(self):
             yield self.check_basic_frame_alignment, engine, parser
 
     def check_frame_comparison(self, engine, parser):
-        skip_if_no_ne(engine)
+        tm.skip_if_no_ne(engine)
         args = product(self.lhs_index_types, repeat=2)
         for r_idx_type, c_idx_type in args:
             df = mkdf(10, 10, data_gen_f=f, r_idx_type=r_idx_type,
@@ -833,7 +829,7 @@ def test_frame_comparison(self):
             yield self.check_frame_comparison, engine, parser
 
     def check_medium_complex_frame_alignment(self, engine, parser):
-        skip_if_no_ne(engine)
+        tm.skip_if_no_ne(engine)
         args = product(self.lhs_index_types, self.index_types,
                        self.index_types, self.index_types)
 
@@ -850,7 +846,7 @@ def test_medium_complex_frame_alignment(self):
             yield self.check_medium_complex_frame_alignment, engine, parser
 
     def check_basic_frame_series_alignment(self, engine, parser):
-        skip_if_no_ne(engine)
+        tm.skip_if_no_ne(engine)
 
         def testit(r_idx_type, c_idx_type, index_name):
             df = mkdf(10, 10, data_gen_f=f, r_idx_type=r_idx_type,
@@ -878,7 +874,7 @@ def test_basic_frame_series_alignment(self):
             yield self.check_basic_frame_series_alignment, engine, parser
 
     def check_basic_series_frame_alignment(self, engine, parser):
-        skip_if_no_ne(engine)
+        tm.skip_if_no_ne(engine)
 
         def testit(r_idx_type, c_idx_type, index_name):
             df = mkdf(10, 7, data_gen_f=f, r_idx_type=r_idx_type,
@@ -911,7 +907,7 @@ def test_basic_series_frame_alignment(self):
             yield self.check_basic_series_frame_alignment, engine, parser
 
     def check_series_frame_commutativity(self, engine, parser):
-        skip_if_no_ne(engine)
+        tm.skip_if_no_ne(engine)
         args = product(self.lhs_index_types, self.index_types, ('+', '*'),
                        ('index', 'columns'))
         for r_idx_type, c_idx_type, op, index_name in args:
@@ -934,7 +930,7 @@ def test_series_frame_commutativity(self):
             yield self.check_series_frame_commutativity, engine, parser
 
     def check_complex_series_frame_alignment(self, engine, parser):
-        skip_if_no_ne(engine)
+        tm.skip_if_no_ne(engine)
 
         import random
         args = product(self.lhs_index_types, self.index_types,
@@ -978,7 +974,7 @@ def test_complex_series_frame_alignment(self):
             yield self.check_complex_series_frame_alignment, engine, parser
 
     def check_performance_warning_for_poor_alignment(self, engine, parser):
-        skip_if_no_ne(engine)
+        tm.skip_if_no_ne(engine)
         df = DataFrame(randn(1000, 10))
         s = Series(randn(10000))
         if engine == 'numexpr':
@@ -1034,7 +1030,7 @@ class TestOperationsNumExprPandas(tm.TestCase):
     @classmethod
     def setUpClass(cls):
         super(TestOperationsNumExprPandas, cls).setUpClass()
-        skip_if_no_ne()
+        tm.skip_if_no_ne()
         cls.engine = 'numexpr'
         cls.parser = 'pandas'
         cls.arith_ops = expr._arith_ops_syms + expr._cmp_ops_syms
@@ -1194,7 +1190,7 @@ def test_assignment_fails(self):
                           local_dict={'df': df, 'df2': df2})
 
     def test_assignment_column(self):
-        skip_if_no_ne('numexpr')
+        tm.skip_if_no_ne('numexpr')
         df = DataFrame(np.random.randn(5, 2), columns=list('ab'))
         orig_df = df.copy()
 
@@ -1345,10 +1341,9 @@ class TestOperationsNumExprPython(TestOperationsNumExprPandas):
     @classmethod
     def setUpClass(cls):
         super(TestOperationsNumExprPython, cls).setUpClass()
-        if not _USE_NUMEXPR:
-            raise nose.SkipTest("numexpr engine not installed")
         cls.engine = 'numexpr'
         cls.parser = 'python'
+        tm.skip_if_no_ne(cls.engine)
         cls.arith_ops = expr._arith_ops_syms + expr._cmp_ops_syms
         cls.arith_ops = filter(lambda x: x not in ('in', 'not in'),
                                cls.arith_ops)
@@ -1435,7 +1430,7 @@ def setUpClass(cls):
 class TestScope(object):
 
     def check_global_scope(self, e, engine, parser):
-        skip_if_no_ne(engine)
+        tm.skip_if_no_ne(engine)
         assert_array_equal(_var_s * 2, pd.eval(e, engine=engine,
                                                parser=parser))
 
@@ -1445,7 +1440,7 @@ def test_global_scope(self):
             yield self.check_global_scope, e, engine, parser
 
     def check_no_new_locals(self, engine, parser):
-        skip_if_no_ne(engine)
+        tm.skip_if_no_ne(engine)
         x = 1
         lcls = locals().copy()
         pd.eval('x + 1', local_dict=lcls, engine=engine, parser=parser)
@@ -1458,7 +1453,7 @@ def test_no_new_locals(self):
             yield self.check_no_new_locals, engine, parser
 
     def check_no_new_globals(self, engine, parser):
-        skip_if_no_ne(engine)
+        tm.skip_if_no_ne(engine)
         x = 1
         gbls = globals().copy()
         pd.eval('x + 1', engine=engine, parser=parser)
@@ -1471,21 +1466,21 @@ def test_no_new_globals(self):
 
 
 def test_invalid_engine():
-    skip_if_no_ne()
+    tm.skip_if_no_ne()
     assertRaisesRegexp(KeyError, 'Invalid engine \'asdf\' passed',
                        pd.eval, 'x + y', local_dict={'x': 1, 'y': 2},
                        engine='asdf')
 
 
 def test_invalid_parser():
-    skip_if_no_ne()
+    tm.skip_if_no_ne()
     assertRaisesRegexp(KeyError, 'Invalid parser \'asdf\' passed',
                        pd.eval, 'x + y', local_dict={'x': 1, 'y': 2},
                        parser='asdf')
 
 
 def check_is_expr_syntax(engine):
-    skip_if_no_ne(engine)
+    tm.skip_if_no_ne(engine)
     s = 1
     valid1 = 's + 1'
     valid2 = '__y + _xx'
@@ -1494,7 +1489,7 @@ def check_is_expr_syntax(engine):
 
 
 def check_is_expr_names(engine):
-    skip_if_no_ne(engine)
+    tm.skip_if_no_ne(engine)
     r, s = 1, 2
     valid = 's + r'
     invalid = '__y + __x'
@@ -1517,7 +1512,7 @@ def test_is_expr_names():
 
 
 def check_disallowed_nodes(engine, parser):
-    skip_if_no_ne(engine)
+    tm.skip_if_no_ne(engine)
     VisitorClass = _parsers[parser]
     uns_ops = VisitorClass.unsupported_nodes
     inst = VisitorClass('x + 1', engine, parser)
@@ -1532,7 +1527,7 @@ def test_disallowed_nodes():
 
 
 def check_syntax_error_exprs(engine, parser):
-    skip_if_no_ne(engine)
+    tm.skip_if_no_ne(engine)
     e = 's +'
     assert_raises(SyntaxError, pd.eval, e, engine=engine, parser=parser)
 
@@ -1543,7 +1538,7 @@ def test_syntax_error_exprs():
 
 
 def check_name_error_exprs(engine, parser):
-    skip_if_no_ne(engine)
+    tm.skip_if_no_ne(engine)
     e = 's + t'
     assert_raises(NameError, pd.eval, e, engine=engine, parser=parser)
 
@@ -1553,6 +1548,33 @@ def test_name_error_exprs():
         yield check_name_error_exprs, engine, parser
 
 
+def check_invalid_numexpr_version(engine, parser):
+    def testit():
+        a, b = 1, 2
+        res = pd.eval('a + b', engine=engine, parser=parser)
+        tm.assert_equal(res, 3)
+
+    if engine == 'numexpr':
+        try:
+            import numexpr as ne
+        except ImportError:
+            raise nose.SkipTest("no numexpr")
+        else:
+            if ne.__version__ < LooseVersion('2.0'):
+                with tm.assertRaisesRegexp(ImportError, "'numexpr' version is "
+                                           ".+, must be >= 2.0"):
+                    testit()
+            else:
+                testit()
+    else:
+        testit()
+
+
+def test_invalid_numexpr_version():
+    for engine, parser in ENGINES_PARSERS:
+        yield check_invalid_numexpr_version, engine, parser
+
+
 if __name__ == '__main__':
     nose.runmodule(argv=[__file__, '-vvs', '-x', '--pdb', '--pdb-failure'],
                    exit=False)
diff --git a/pandas/tests/test_frame.py b/pandas/tests/test_frame.py
--- a/pandas/tests/test_frame.py
+++ b/pandas/tests/test_frame.py
@@ -11,6 +11,7 @@
 import functools
 import itertools
 from itertools import product
+from distutils.version import LooseVersion
 
 from pandas.compat import(
     map, zip, range, long, lrange, lmap, lzip,
@@ -12014,14 +12015,6 @@ def test_concat_empty_dataframe_dtypes(self):
         self.assertEqual(result['c'].dtype, np.float64)
 
 
-def skip_if_no_ne(engine='numexpr'):
-    if engine == 'numexpr':
-        try:
-            import numexpr as ne
-        except ImportError:
-            raise nose.SkipTest("cannot query engine numexpr when numexpr not "
-                                "installed")
-
 
 def skip_if_no_pandas_parser(parser):
     if parser != 'pandas':
@@ -12030,7 +12023,7 @@ def skip_if_no_pandas_parser(parser):
 
 class TestDataFrameQueryWithMultiIndex(object):
     def check_query_with_named_multiindex(self, parser, engine):
-        skip_if_no_ne(engine)
+        tm.skip_if_no_ne(engine)
         a = tm.choice(['red', 'green'], size=10)
         b = tm.choice(['eggs', 'ham'], size=10)
         index = MultiIndex.from_arrays([a, b], names=['color', 'food'])
@@ -12084,7 +12077,7 @@ def test_query_with_named_multiindex(self):
             yield self.check_query_with_named_multiindex, parser, engine
 
     def check_query_with_unnamed_multiindex(self, parser, engine):
-        skip_if_no_ne(engine)
+        tm.skip_if_no_ne(engine)
         a = tm.choice(['red', 'green'], size=10)
         b = tm.choice(['eggs', 'ham'], size=10)
         index = MultiIndex.from_arrays([a, b])
@@ -12176,7 +12169,7 @@ def test_query_with_unnamed_multiindex(self):
             yield self.check_query_with_unnamed_multiindex, parser, engine
 
     def check_query_with_partially_named_multiindex(self, parser, engine):
-        skip_if_no_ne(engine)
+        tm.skip_if_no_ne(engine)
         a = tm.choice(['red', 'green'], size=10)
         b = np.arange(10)
         index = MultiIndex.from_arrays([a, b])
@@ -12242,7 +12235,7 @@ def test_raise_on_panel_with_multiindex(self):
             yield self.check_raise_on_panel_with_multiindex, parser, engine
 
     def check_raise_on_panel_with_multiindex(self, parser, engine):
-        skip_if_no_ne()
+        tm.skip_if_no_ne()
         p = tm.makePanel(7)
         p.items = tm.makeCustomIndex(len(p.items), nlevels=2)
         with tm.assertRaises(NotImplementedError):
@@ -12253,7 +12246,7 @@ def test_raise_on_panel4d_with_multiindex(self):
             yield self.check_raise_on_panel4d_with_multiindex, parser, engine
 
     def check_raise_on_panel4d_with_multiindex(self, parser, engine):
-        skip_if_no_ne()
+        tm.skip_if_no_ne()
         p4d = tm.makePanel4D(7)
         p4d.items = tm.makeCustomIndex(len(p4d.items), nlevels=2)
         with tm.assertRaises(NotImplementedError):
@@ -12267,7 +12260,7 @@ def setUpClass(cls):
         super(TestDataFrameQueryNumExprPandas, cls).setUpClass()
         cls.engine = 'numexpr'
         cls.parser = 'pandas'
-        skip_if_no_ne()
+        tm.skip_if_no_ne()
 
     @classmethod
     def tearDownClass(cls):
@@ -12496,7 +12489,7 @@ def setUpClass(cls):
         super(TestDataFrameQueryNumExprPython, cls).setUpClass()
         cls.engine = 'numexpr'
         cls.parser = 'python'
-        skip_if_no_ne(cls.engine)
+        tm.skip_if_no_ne(cls.engine)
         cls.frame = _frame.copy()
 
     def test_date_query_method(self):
@@ -12615,7 +12608,7 @@ def setUpClass(cls):
 
 class TestDataFrameQueryStrings(object):
     def check_str_query_method(self, parser, engine):
-        skip_if_no_ne(engine)
+        tm.skip_if_no_ne(engine)
         df = DataFrame(randn(10, 1), columns=['b'])
         df['strings'] = Series(list('aabbccddee'))
         expect = df[df.strings == 'a']
@@ -12659,7 +12652,7 @@ def test_str_list_query_method(self):
             yield self.check_str_list_query_method, parser, engine
 
     def check_str_list_query_method(self, parser, engine):
-        skip_if_no_ne(engine)
+        tm.skip_if_no_ne(engine)
         df = DataFrame(randn(10, 1), columns=['b'])
         df['strings'] = Series(list('aabbccddee'))
         expect = df[df.strings.isin(['a', 'b'])]
@@ -12698,7 +12691,7 @@ def check_str_list_query_method(self, parser, engine):
             assert_frame_equal(res, expect)
 
     def check_query_with_string_columns(self, parser, engine):
-        skip_if_no_ne(engine)
+        tm.skip_if_no_ne(engine)
         df = DataFrame({'a': list('aaaabbbbcccc'),
                         'b': list('aabbccddeeff'),
                         'c': np.random.randint(5, size=12),
@@ -12723,7 +12716,7 @@ def test_query_with_string_columns(self):
             yield self.check_query_with_string_columns, parser, engine
 
     def check_object_array_eq_ne(self, parser, engine):
-        skip_if_no_ne(engine)
+        tm.skip_if_no_ne(engine)
         df = DataFrame({'a': list('aaaabbbbcccc'),
                         'b': list('aabbccddeeff'),
                         'c': np.random.randint(5, size=12),
@@ -12741,7 +12734,7 @@ def test_object_array_eq_ne(self):
             yield self.check_object_array_eq_ne, parser, engine
 
     def check_query_with_nested_strings(self, parser, engine):
-        skip_if_no_ne(engine)
+        tm.skip_if_no_ne(engine)
         skip_if_no_pandas_parser(parser)
         from pandas.compat import StringIO
         raw = """id          event          timestamp
@@ -12776,7 +12769,7 @@ def setUpClass(cls):
         super(TestDataFrameEvalNumExprPandas, cls).setUpClass()
         cls.engine = 'numexpr'
         cls.parser = 'pandas'
-        skip_if_no_ne()
+        tm.skip_if_no_ne()
 
     def setUp(self):
         self.frame = DataFrame(randn(10, 3), columns=list('abc'))
@@ -12803,7 +12796,7 @@ def setUpClass(cls):
         super(TestDataFrameEvalNumExprPython, cls).setUpClass()
         cls.engine = 'numexpr'
         cls.parser = 'python'
-        skip_if_no_ne()
+        tm.skip_if_no_ne(cls.engine)
 
 
 class TestDataFrameEvalPythonPandas(TestDataFrameEvalNumExprPandas):
diff --git a/pandas/util/testing.py b/pandas/util/testing.py
--- a/pandas/util/testing.py
+++ b/pandas/util/testing.py
@@ -22,7 +22,7 @@
 import numpy as np
 
 import pandas as pd
-from pandas.core.common import isnull, _is_sequence
+from pandas.core.common import _is_sequence
 import pandas.core.index as index
 import pandas.core.series as series
 import pandas.core.frame as frame
@@ -1391,3 +1391,21 @@ def assert_produces_warning(expected_warning=Warning, filter_level="always"):
                                  % expected_warning.__name__)
         assert not extra_warnings, ("Caused unexpected warning(s): %r."
                                     % extra_warnings)
+
+
+def skip_if_no_ne(engine='numexpr'):
+    import nose
+    _USE_NUMEXPR = pd.computation.expressions._USE_NUMEXPR
+
+    if engine == 'numexpr':
+        try:
+            import numexpr as ne
+        except ImportError:
+            raise nose.SkipTest("numexpr not installed")
+
+        if not _USE_NUMEXPR:
+            raise nose.SkipTest("numexpr disabled")
+
+        if ne.__version__ < LooseVersion('2.0'):
+            raise nose.SkipTest("numexpr version too low: "
+                                "%s" % ne.__version__)