Skip to content

Commit 504ad46

Browse files
committed
COMPAT: .query/.eval should work w/o numexpr being installed if possible
closes #12749 closes #12864
1 parent 1c8816f commit 504ad46

File tree

5 files changed

+83
-20
lines changed

5 files changed

+83
-20
lines changed

.travis.yml

+6-2
Original file line numberDiff line numberDiff line change
@@ -152,25 +152,29 @@ before_install:
152152
- export DISPLAY=:99.0
153153

154154
install:
155-
- echo "install"
155+
- echo "install start"
156156
- ci/prep_ccache.sh
157157
- ci/install_travis.sh
158158
- ci/submit_ccache.sh
159+
- echo "install done"
159160

160161
before_script:
161162
- source activate pandas && pip install codecov
162163
- ci/install_db.sh
163164

164165
script:
165-
- echo "script"
166+
- echo "script start"
166167
- ci/run_build_docs.sh
167168
- ci/script.sh
168169
- ci/lint.sh
170+
- echo "script done"
169171

170172
after_success:
171173
- source activate pandas && codecov
172174

173175
after_script:
176+
- echo "after_script start"
174177
- ci/install_test.sh
175178
- source activate pandas && ci/print_versions.py
176179
- ci/print_skipped.py /tmp/nosetests.xml
180+
- echo "after_script done"

doc/source/whatsnew/v0.18.1.txt

+2
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,8 @@ API changes
131131

132132

133133

134+
- The default for ``.query()/.eval()`` is now ``engine=None``, which will use ``numexpr`` if it's installed; otherwise it will fallback to the ``python`` engine. This mimics the pre-0.18.1 behavior if ``numexpr`` is installed (and which Previously, if numexpr was not installed, ``.query()/.eval()`` would raise). (:issue:`12749`)
135+
134136

135137
- ``CParserError`` is now a ``ValueError`` instead of just an ``Exception`` (:issue:`12551`)
136138
- ``read_csv`` no longer allows a combination of strings and integers for the ``usecols`` parameter (:issue:`12678`)

pandas/computation/eval.py

+18-3
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,19 @@ def _check_engine(engine):
2626
* If an invalid engine is passed
2727
ImportError
2828
* If numexpr was requested but doesn't exist
29+
30+
Returns
31+
-------
32+
string engine
33+
2934
"""
35+
36+
if engine is None:
37+
if _NUMEXPR_INSTALLED:
38+
engine = 'numexpr'
39+
else:
40+
engine = 'python'
41+
3042
if engine not in _engines:
3143
raise KeyError('Invalid engine {0!r} passed, valid engines are'
3244
' {1}'.format(engine, list(_engines.keys())))
@@ -41,6 +53,8 @@ def _check_engine(engine):
4153
"engine='numexpr' for query/eval "
4254
"if 'numexpr' is not installed")
4355

56+
return engine
57+
4458

4559
def _check_parser(parser):
4660
"""Make sure a valid parser is passed.
@@ -131,7 +145,7 @@ def _check_for_locals(expr, stack_level, parser):
131145
raise SyntaxError(msg)
132146

133147

134-
def eval(expr, parser='pandas', engine='numexpr', truediv=True,
148+
def eval(expr, parser='pandas', engine=None, truediv=True,
135149
local_dict=None, global_dict=None, resolvers=(), level=0,
136150
target=None, inplace=None):
137151
"""Evaluate a Python expression as a string using various backends.
@@ -160,10 +174,11 @@ def eval(expr, parser='pandas', engine='numexpr', truediv=True,
160174
``'python'`` parser to retain strict Python semantics. See the
161175
:ref:`enhancing performance <enhancingperf.eval>` documentation for
162176
more details.
163-
engine : string, default 'numexpr', {'python', 'numexpr'}
177+
engine : string or None, default 'numexpr', {'python', 'numexpr'}
164178
165179
The engine used to evaluate the expression. Supported engines are
166180
181+
- None : tries to use ``numexpr``, falls back to ``python``
167182
- ``'numexpr'``: This default engine evaluates pandas objects using
168183
numexpr for large speed ups in complex expressions
169184
with large frames.
@@ -230,7 +245,7 @@ def eval(expr, parser='pandas', engine='numexpr', truediv=True,
230245
first_expr = True
231246
for expr in exprs:
232247
expr = _convert_expression(expr)
233-
_check_engine(engine)
248+
engine = _check_engine(engine)
234249
_check_parser(parser)
235250
_check_resolvers(resolvers)
236251
_check_for_locals(expr, level, parser)

pandas/tests/frame/test_query_eval.py

+50-3
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
makeCustomDataframe as mkdf)
2020

2121
import pandas.util.testing as tm
22+
from pandas.computation import _NUMEXPR_INSTALLED
2223

2324
from pandas.tests.frame.common import TestData
2425

@@ -34,13 +35,59 @@ def skip_if_no_pandas_parser(parser):
3435

3536
def skip_if_no_ne(engine='numexpr'):
3637
if engine == 'numexpr':
37-
try:
38-
import numexpr as ne # noqa
39-
except ImportError:
38+
if not _NUMEXPR_INSTALLED:
4039
raise nose.SkipTest("cannot query engine numexpr when numexpr not "
4140
"installed")
4241

4342

43+
class TestCompat(tm.TestCase):
44+
45+
def setUp(self):
46+
self.df = DataFrame({'A': [1, 2, 3]})
47+
self.expected1 = self.df[self.df.A > 0]
48+
self.expected2 = self.df.A + 1
49+
50+
def test_query_default(self):
51+
52+
# GH 12749
53+
# this should always work, whether _NUMEXPR_INSTALLED or not
54+
df = self.df
55+
result = df.query('A>0')
56+
assert_frame_equal(result, self.expected1)
57+
result = df.eval('A+1')
58+
assert_series_equal(result, self.expected2, check_names=False)
59+
60+
def test_query_None(self):
61+
62+
df = self.df
63+
result = df.query('A>0', engine=None)
64+
assert_frame_equal(result, self.expected1)
65+
result = df.eval('A+1', engine=None)
66+
assert_series_equal(result, self.expected2, check_names=False)
67+
68+
def test_query_python(self):
69+
70+
df = self.df
71+
result = df.query('A>0', engine='python')
72+
assert_frame_equal(result, self.expected1)
73+
result = df.eval('A+1', engine='python')
74+
assert_series_equal(result, self.expected2, check_names=False)
75+
76+
def test_query_numexpr(self):
77+
78+
df = self.df
79+
if _NUMEXPR_INSTALLED:
80+
result = df.query('A>0', engine='numexpr')
81+
assert_frame_equal(result, self.expected1)
82+
result = df.eval('A+1', engine='numexpr')
83+
assert_series_equal(result, self.expected2, check_names=False)
84+
else:
85+
self.assertRaises(ImportError,
86+
lambda: df.query('A>0', engine='numexpr'))
87+
self.assertRaises(ImportError,
88+
lambda: df.eval('A+1', engine='numexpr'))
89+
90+
4491
class TestDataFrameEval(tm.TestCase, TestData):
4592

4693
_multiprocess_can_split_ = True

pandas/util/testing.py

+7-12
Original file line numberDiff line numberDiff line change
@@ -329,21 +329,16 @@ def _incompat_bottleneck_version(method):
329329

330330

331331
def skip_if_no_ne(engine='numexpr'):
332-
import nose
333-
_USE_NUMEXPR = pd.computation.expressions._USE_NUMEXPR
332+
from pandas.computation.expressions import (_USE_NUMEXPR,
333+
_NUMEXPR_INSTALLED)
334334

335335
if engine == 'numexpr':
336-
try:
337-
import numexpr as ne
338-
except ImportError:
339-
raise nose.SkipTest("numexpr not installed")
340-
341336
if not _USE_NUMEXPR:
342-
raise nose.SkipTest("numexpr disabled")
343-
344-
if ne.__version__ < LooseVersion('2.0'):
345-
raise nose.SkipTest("numexpr version too low: "
346-
"%s" % ne.__version__)
337+
import nose
338+
raise nose.SkipTest("numexpr enabled->{enabled}, "
339+
"installed->{installed}".format(
340+
enabled=_USE_NUMEXPR,
341+
installed=_NUMEXPR_INSTALLED))
347342

348343

349344
def _skip_if_has_locale():

0 commit comments

Comments
 (0)