diff --git a/pandas/tests/computation/test_eval.py b/pandas/tests/computation/test_eval.py index c35cdd39fcaf7..64cf896b84b36 100644 --- a/pandas/tests/computation/test_eval.py +++ b/pandas/tests/computation/test_eval.py @@ -53,11 +53,14 @@ params=( pytest.param( engine, - marks=pytest.mark.skipif( - engine == "numexpr" and not USE_NUMEXPR, - reason=f"numexpr enabled->{USE_NUMEXPR}, " - f"installed->{NUMEXPR_INSTALLED}", - ), + marks=[ + pytest.mark.skipif( + engine == "numexpr" and not USE_NUMEXPR, + reason=f"numexpr enabled->{USE_NUMEXPR}, " + f"installed->{NUMEXPR_INSTALLED}", + ), + td.skip_if_no_ne, + ], ) for engine in ENGINES ) @@ -145,10 +148,12 @@ def lhs(request): midhs = lhs -@td.skip_if_no_ne -class TestEvalNumexprPandas: - engine = "numexpr" - parser = "pandas" +class TestEval: + @pytest.fixture(autouse=True) + def set_engine_parser_attrs(self, engine, parser): + # Older tests look for these as attributes, so we set them here. + self.engine = engine + self.parser = parser @classmethod def setup_class(cls): @@ -184,47 +189,18 @@ def test_complex_cmp_ops(self, cmp1, cmp2, binop, lhs, rhs): self.check_equal(result, expected) @pytest.mark.parametrize("cmp_op", expr.CMP_OPS_SYMS) - def test_simple_cmp_ops(self, cmp_op): - bool_lhses = ( - DataFrame(tm.randbool(size=(10, 5))), - Series(tm.randbool((5,))), - tm.randbool(), - ) - bool_rhses = ( - DataFrame(tm.randbool(size=(10, 5))), - Series(tm.randbool((5,))), - tm.randbool(), - ) + def test_simple_cmp_ops(self, cmp_op, lhs, rhs): + lhs = lhs < 0 + rhs = rhs < 0 if self.parser == "python" and cmp_op in ["in", "not in"]: msg = "'(In|NotIn)' nodes are not implemented" - for lhs, rhs in product(bool_lhses, bool_rhses): - with pytest.raises(NotImplementedError, match=msg): - self.check_simple_cmp_op(lhs, cmp_op, rhs) + with pytest.raises(NotImplementedError, match=msg): + self.check_simple_cmp_op(lhs, cmp_op, rhs) return - for lhs, rhs in product(bool_lhses, bool_rhses): - self.check_simple_cmp_op(lhs, cmp_op, rhs) - - @pytest.mark.parametrize("op", _good_arith_ops) - def test_binary_arith_ops(self, op, lhs, rhs, request): - self.check_binary_arith_op(lhs, op, rhs) - - def test_modulus(self, lhs, rhs): - self.check_modulus(lhs, "%", rhs) - - def test_floor_division(self, lhs, rhs): - self.check_floor_division(lhs, "//", rhs) - - @td.skip_if_windows - def test_pow(self, lhs, rhs): - # odd failure on win32 platform, so skip - self.check_pow(lhs, "**", rhs) - - @pytest.mark.parametrize("op", expr.CMP_OPS_SYMS) - def test_single_invert_op(self, op, lhs): - self.check_single_invert_op(lhs, op) + self.check_simple_cmp_op(lhs, cmp_op, rhs) @pytest.mark.parametrize("op", expr.CMP_OPS_SYMS) def test_compound_invert_op(self, op, lhs, rhs, request): @@ -266,11 +242,16 @@ def check_equal(self, result, expected): assert result == expected def check_chained_cmp_op(self, lhs, cmp1, mid, cmp2, rhs): - def check_operands(left, right, cmp_op): - return _eval_single_bin(left, cmp_op, right, self.engine) - lhs_new = check_operands(lhs, mid, cmp1) - rhs_new = check_operands(mid, rhs, cmp2) + if self.parser == "python": + ex1 = f"lhs {cmp1} mid {cmp2} rhs" + msg = "'BoolOp' nodes are not implemented" + with pytest.raises(NotImplementedError, match=msg): + pd.eval(ex1, engine=self.engine, parser=self.parser) + return + + lhs_new = _eval_single_bin(lhs, cmp1, mid, self.engine) + rhs_new = _eval_single_bin(mid, cmp2, rhs, self.engine) if lhs_new is not None and rhs_new is not None: ex1 = f"lhs {cmp1} mid {cmp2} rhs" @@ -306,7 +287,8 @@ def check_simple_cmp_op(self, lhs, cmp1, rhs): result = pd.eval(ex, engine=self.engine, parser=self.parser) self.check_equal(result, expected) - def check_binary_arith_op(self, lhs, arith1, rhs): + @pytest.mark.parametrize("arith1", _good_arith_ops) + def test_binary_arith_ops(self, arith1, lhs, rhs): ex = f"lhs {arith1} rhs" result = pd.eval(ex, engine=self.engine, parser=self.parser) expected = _eval_single_bin(lhs, arith1, rhs, self.engine) @@ -339,20 +321,24 @@ def check_alignment(self, result, nlhs, ghs, op): # modulus, pow, and floor division require special casing - def check_modulus(self, lhs, arith1, rhs): - ex = f"lhs {arith1} rhs" + def test_modulus(self, lhs, rhs): + ex = r"lhs % rhs" result = pd.eval(ex, engine=self.engine, parser=self.parser) expected = lhs % rhs - tm.assert_almost_equal(result, expected) - expected = self.ne.evaluate(f"expected {arith1} rhs") - if isinstance(result, (DataFrame, Series)): - tm.assert_almost_equal(result.values, expected) + + if self.engine == "numexpr": + expected = self.ne.evaluate(r"expected % rhs") + if isinstance(result, (DataFrame, Series)): + tm.assert_almost_equal(result.values, expected) + else: + tm.assert_almost_equal(result, expected.item()) else: - tm.assert_almost_equal(result, expected.item()) + expected = _eval_single_bin(expected, "%", rhs, self.engine) + tm.assert_almost_equal(result, expected) - def check_floor_division(self, lhs, arith1, rhs): - ex = f"lhs {arith1} rhs" + def test_floor_division(self, lhs, rhs): + ex = "lhs // rhs" if self.engine == "python": res = pd.eval(ex, engine=self.engine, parser=self.parser) @@ -371,8 +357,10 @@ def check_floor_division(self, lhs, arith1, rhs): parser=self.parser, ) - def check_pow(self, lhs, arith1, rhs): - ex = f"lhs {arith1} rhs" + @td.skip_if_windows + def test_pow(self, lhs, rhs): + # odd failure on win32 platform, so skip + ex = "lhs ** rhs" expected = _eval_single_bin(lhs, "**", rhs, self.engine) result = pd.eval(ex, engine=self.engine, parser=self.parser) @@ -387,19 +375,19 @@ def check_pow(self, lhs, arith1, rhs): else: tm.assert_almost_equal(result, expected) - ex = f"(lhs {arith1} rhs) {arith1} rhs" + ex = "(lhs ** rhs) ** rhs" result = pd.eval(ex, engine=self.engine, parser=self.parser) middle = _eval_single_bin(lhs, "**", rhs, self.engine) expected = _eval_single_bin(middle, "**", rhs, self.engine) tm.assert_almost_equal(result, expected) - def check_single_invert_op(self, elem, cmp1): + def check_single_invert_op(self, lhs): # simple try: - elb = elem.astype(bool) + elb = lhs.astype(bool) except AttributeError: - elb = np.array([bool(elem)]) + elb = np.array([bool(lhs)]) expected = ~elb result = pd.eval("~elb", engine=self.engine, parser=self.parser) tm.assert_almost_equal(expected, result) @@ -766,43 +754,6 @@ def test_disallow_python_keywords(self): df.query("lambda == 0") -@td.skip_if_no_ne -class TestEvalNumexprPython(TestEvalNumexprPandas): - engine = "numexpr" - parser = "python" - - def check_chained_cmp_op(self, lhs, cmp1, mid, cmp2, rhs): - ex1 = f"lhs {cmp1} mid {cmp2} rhs" - msg = "'BoolOp' nodes are not implemented" - with pytest.raises(NotImplementedError, match=msg): - pd.eval(ex1, engine=self.engine, parser=self.parser) - - -class TestEvalPythonPython(TestEvalNumexprPython): - engine = "python" - parser = "python" - - def check_modulus(self, lhs, arith1, rhs): - ex = f"lhs {arith1} rhs" - result = pd.eval(ex, engine=self.engine, parser=self.parser) - - expected = lhs % rhs - tm.assert_almost_equal(result, expected) - - expected = _eval_single_bin(expected, arith1, rhs, self.engine) - tm.assert_almost_equal(result, expected) - - -class TestEvalPythonPandas(TestEvalPythonPython): - engine = "python" - parser = "pandas" - - def check_chained_cmp_op(self, lhs, cmp1, mid, cmp2, rhs): - # FIXME: by calling this parent class method, we are using the parent - # class's "engine" and "parser", which I don't think is what we want. - TestEvalNumexprPandas.check_chained_cmp_op(self, lhs, cmp1, mid, cmp2, rhs) - - f = lambda *args, **kwargs: np.random.randn()