From cd560694c50d3088a0b08e04842c2541587e89e0 Mon Sep 17 00:00:00 2001 From: Alex-Blade <44120047+Alex-Blade@users.noreply.github.com> Date: Mon, 6 Jun 2022 01:26:55 +0300 Subject: [PATCH] Backport PR #47085: BUG: Eval scopes ignoring empty dictionaries (#47084) --- doc/source/whatsnew/v1.4.3.rst | 1 + pandas/core/computation/pytables.py | 2 +- pandas/core/computation/scope.py | 6 ++++-- pandas/tests/computation/test_eval.py | 15 +++++++++++++++ 4 files changed, 21 insertions(+), 3 deletions(-) diff --git a/doc/source/whatsnew/v1.4.3.rst b/doc/source/whatsnew/v1.4.3.rst index 54cad82366e43..18d0a0fd97d1f 100644 --- a/doc/source/whatsnew/v1.4.3.rst +++ b/doc/source/whatsnew/v1.4.3.rst @@ -30,6 +30,7 @@ Fixed regressions Bug fixes ~~~~~~~~~ +- Bug in :meth:`pd.eval`, :meth:`DataFrame.eval` and :meth:`DataFrame.query` where passing empty ``local_dict`` or ``global_dict`` was treated as passing ``None`` (:issue:`47084`) - Most I/O methods do no longer suppress ``OSError`` and ``ValueError`` when closing file handles (:issue:`47136`) - diff --git a/pandas/core/computation/pytables.py b/pandas/core/computation/pytables.py index 3e041c088f566..0ed0046d36678 100644 --- a/pandas/core/computation/pytables.py +++ b/pandas/core/computation/pytables.py @@ -563,7 +563,7 @@ def __init__( self._visitor = None # capture the environment if needed - local_dict: DeepChainMap[Any, Any] = DeepChainMap() + local_dict: DeepChainMap[Any, Any] | None = None if isinstance(where, PyTablesExpr): local_dict = where.env.scope diff --git a/pandas/core/computation/scope.py b/pandas/core/computation/scope.py index a561824f868f2..32e979eae991e 100644 --- a/pandas/core/computation/scope.py +++ b/pandas/core/computation/scope.py @@ -133,11 +133,13 @@ def __init__( # shallow copy here because we don't want to replace what's in # scope when we align terms (alignment accesses the underlying # numpy array of pandas objects) - scope_global = self.scope.new_child((global_dict or frame.f_globals).copy()) + scope_global = self.scope.new_child( + (global_dict if global_dict is not None else frame.f_globals).copy() + ) self.scope = DeepChainMap(scope_global) if not isinstance(local_dict, Scope): scope_local = self.scope.new_child( - (local_dict or frame.f_locals).copy() + (local_dict if local_dict is not None else frame.f_locals).copy() ) self.scope = DeepChainMap(scope_local) finally: diff --git a/pandas/tests/computation/test_eval.py b/pandas/tests/computation/test_eval.py index 8fa28300b8345..3517068b3d0cc 100644 --- a/pandas/tests/computation/test_eval.py +++ b/pandas/tests/computation/test_eval.py @@ -43,6 +43,7 @@ from pandas.core.computation.ops import ( ARITH_OPS_SYMS, SPECIAL_CASE_ARITH_OPS_SYMS, + UndefinedVariableError, _binary_math_ops, _binary_ops_dict, _unary_math_ops, @@ -1747,6 +1748,20 @@ def test_no_new_globals(self, engine, parser): gbls2 = globals().copy() assert gbls == gbls2 + def test_empty_locals(self, engine, parser): + # GH 47084 + x = 1 # noqa: F841 + msg = "name 'x' is not defined" + with pytest.raises(UndefinedVariableError, match=msg): + pd.eval("x + 1", engine=engine, parser=parser, local_dict={}) + + def test_empty_globals(self, engine, parser): + # GH 47084 + msg = "name '_var_s' is not defined" + e = "_var_s * 2" + with pytest.raises(UndefinedVariableError, match=msg): + pd.eval(e, engine=engine, parser=parser, global_dict={}) + @td.skip_if_no_ne def test_invalid_engine():