diff --git a/doc/source/release.rst b/doc/source/release.rst index 369f83066ed0d..def97ed41b906 100644 --- a/doc/source/release.rst +++ b/doc/source/release.rst @@ -136,6 +136,8 @@ API Changes - A tuple passed to ``DataFame.sort_index`` will be interpreted as the levels of the index, rather than requiring a list of tuple (:issue:`4370`) +- Fix a bug where invalid eval/query operations would blow the stack (:issue:`5198`) + Deprecations ~~~~~~~~~~~~ diff --git a/pandas/computation/expr.py b/pandas/computation/expr.py index 1c40dc9930856..353c58c23febd 100644 --- a/pandas/computation/expr.py +++ b/pandas/computation/expr.py @@ -377,6 +377,11 @@ def _possibly_evaluate_binop(self, op, op_class, lhs, rhs, '<=', '>=')): res = op(lhs, rhs) + if res.has_invalid_return_type: + raise TypeError("unsupported operand type(s) for {0}:" + " '{1}' and '{2}'".format(res.op, lhs.type, + rhs.type)) + if self.engine != 'pytables': if (res.op in _cmp_ops_syms and getattr(lhs, 'is_datetime', False) diff --git a/pandas/computation/ops.py b/pandas/computation/ops.py index 041ab77bb61f4..1f57c459149ad 100644 --- a/pandas/computation/ops.py +++ b/pandas/computation/ops.py @@ -169,7 +169,7 @@ def name(self): class Op(StringMixin): - """Hold an operator of unknown arity + """Hold an operator of arbitrary arity """ def __init__(self, op, operands, *args, **kwargs): @@ -195,6 +195,16 @@ def return_type(self): return np.bool_ return _result_type_many(*(term.type for term in com.flatten(self))) + @property + def has_invalid_return_type(self): + types = self.operand_types + obj_dtype_set = frozenset([np.dtype('object')]) + return self.return_type == object and types - obj_dtype_set + + @property + def operand_types(self): + return frozenset(term.type for term in com.flatten(self)) + @property def isscalar(self): return all(operand.isscalar for operand in self.operands) @@ -412,6 +422,10 @@ def _disallow_scalar_only_bool_ops(self): raise NotImplementedError("cannot evaluate scalar only bool ops") +def isnumeric(dtype): + return issubclass(np.dtype(dtype).type, np.number) + + class Div(BinOp): """Div operator to special case casting. @@ -428,6 +442,12 @@ class Div(BinOp): def __init__(self, lhs, rhs, truediv, *args, **kwargs): super(Div, self).__init__('/', lhs, rhs, *args, **kwargs) + if not isnumeric(lhs.return_type) or not isnumeric(rhs.return_type): + raise TypeError("unsupported operand type(s) for {0}:" + " '{1}' and '{2}'".format(self.op, + lhs.return_type, + rhs.return_type)) + if truediv or PY3: _cast_inplace(com.flatten(self), np.float_) diff --git a/pandas/tests/test_frame.py b/pandas/tests/test_frame.py index 8a4207da76c52..4391de0ebfe58 100644 --- a/pandas/tests/test_frame.py +++ b/pandas/tests/test_frame.py @@ -13243,6 +13243,16 @@ def test_bool_arith_expr(self): expect = self.frame.a[self.frame.a < 1] + self.frame.b assert_series_equal(res, expect) + def test_invalid_type_for_operator_raises(self): + df = DataFrame({'a': [1, 2], 'b': ['c', 'd']}) + ops = '+', '-', '*', '/' + for op in ops: + with tm.assertRaisesRegexp(TypeError, + "unsupported operand type\(s\) for " + ".+: '.+' and '.+'"): + df.eval('a {0} b'.format(op), engine=self.engine, + parser=self.parser) + class TestDataFrameEvalNumExprPython(TestDataFrameEvalNumExprPandas):