Skip to content

ENH: Move UndefinedVariableError to error/__init__.py per GH27656 #47338

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/source/reference/testing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ Exceptions and warnings
errors.SettingWithCopyError
errors.SettingWithCopyWarning
errors.SpecificationError
errors.UndefinedVariableError
errors.UnsortedIndexError
errors.UnsupportedFunctionCall

Expand Down
2 changes: 1 addition & 1 deletion doc/source/whatsnew/v1.5.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ Other enhancements
- A :class:`errors.PerformanceWarning` is now thrown when using ``string[pyarrow]`` dtype with methods that don't dispatch to ``pyarrow.compute`` methods (:issue:`42613`)
- Added ``numeric_only`` argument to :meth:`Resampler.sum`, :meth:`Resampler.prod`, :meth:`Resampler.min`, :meth:`Resampler.max`, :meth:`Resampler.first`, and :meth:`Resampler.last` (:issue:`46442`)
- ``times`` argument in :class:`.ExponentialMovingWindow` now accepts ``np.timedelta64`` (:issue:`47003`)
- :class:`DataError`, :class:`SpecificationError`, :class:`SettingWithCopyError`, :class:`SettingWithCopyWarning`, and :class:`NumExprClobberingError` are now exposed in ``pandas.errors`` (:issue:`27656`)
- :class:`DataError`, :class:`SpecificationError`, :class:`SettingWithCopyError`, :class:`SettingWithCopyWarning`, :class:`NumExprClobberingError`, :class:`UndefinedVariableError` are now exposed in ``pandas.errors`` (:issue:`27656`)
- Added ``check_like`` argument to :func:`testing.assert_series_equal` (:issue:`47247`)

.. ---------------------------------------------------------------------------
Expand Down
2 changes: 1 addition & 1 deletion pandas/core/computation/expr.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import numpy as np

from pandas.compat import PY39
from pandas.errors import UndefinedVariableError

import pandas.core.common as com
from pandas.core.computation.ops import (
Expand All @@ -35,7 +36,6 @@
Op,
Term,
UnaryOp,
UndefinedVariableError,
is_term,
)
from pandas.core.computation.parsing import (
Expand Down
14 changes: 0 additions & 14 deletions pandas/core/computation/ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,20 +65,6 @@
LOCAL_TAG = "__pd_eval_local_"


class UndefinedVariableError(NameError):
"""
NameError subclass for local variables.
"""

def __init__(self, name: str, is_local: bool | None = None) -> None:
base_msg = f"{repr(name)} is not defined"
if is_local:
msg = f"local variable {base_msg}"
else:
msg = f"name {base_msg}"
super().__init__(msg)


class Term:
def __new__(cls, name, env, side=None, encoding=None):
klass = Constant if not isinstance(name, str) else cls
Expand Down
6 changes: 2 additions & 4 deletions pandas/core/computation/pytables.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
)
from pandas._typing import npt
from pandas.compat.chainmap import DeepChainMap
from pandas.errors import UndefinedVariableError

from pandas.core.dtypes.common import is_list_like

Expand All @@ -24,10 +25,7 @@
)
from pandas.core.computation.common import ensure_decoded
from pandas.core.computation.expr import BaseExprVisitor
from pandas.core.computation.ops import (
UndefinedVariableError,
is_term,
)
from pandas.core.computation.ops import is_term
from pandas.core.construction import extract_array
from pandas.core.indexes.base import Index

Expand Down
4 changes: 1 addition & 3 deletions pandas/core/computation/scope.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

from pandas._libs.tslibs import Timestamp
from pandas.compat.chainmap import DeepChainMap
from pandas.errors import UndefinedVariableError


def ensure_scope(
Expand Down Expand Up @@ -207,9 +208,6 @@ def resolve(self, key: str, is_local: bool):
# e.g., df[df > 0]
return self.temps[key]
except KeyError as err:
# runtime import because ops imports from scope
from pandas.core.computation.ops import UndefinedVariableError

raise UndefinedVariableError(key, is_local) from err

def swapkey(self, old_key: str, new_key: str, new_value=None) -> None:
Expand Down
27 changes: 27 additions & 0 deletions pandas/errors/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""
Expose public exceptions & warnings
"""
from __future__ import annotations

from pandas._config.config import OptionError # noqa:F401

Expand Down Expand Up @@ -326,3 +327,29 @@ class NumExprClobberingError(NameError):
>>> pd.eval("sin + a", engine='numexpr') # doctest: +SKIP
... # NumExprClobberingError: Variables in expression "(sin) + (a)" overlap...
"""


class UndefinedVariableError(NameError):
"""
Exception is raised when trying to use an undefined variable name in a method
like query or eval. It will also specific whether the undefined variable is
local or not.

Examples
--------
>>> df = pd.DataFrame({'A': [1, 1, 1]})
>>> df.query("A > x") # doctest: +SKIP
... # UndefinedVariableError: name 'x' is not defined
>>> df.query("A > @y") # doctest: +SKIP
... # UndefinedVariableError: local variable 'y' is not defined
>>> pd.eval('x + 1') # doctest: +SKIP
... # UndefinedVariableError: name 'x' is not defined
"""

def __init__(self, name: str, is_local: bool | None = None) -> None:
base_msg = f"{repr(name)} is not defined"
if is_local:
msg = f"local variable {base_msg}"
else:
msg = f"name {base_msg}"
super().__init__(msg)
2 changes: 1 addition & 1 deletion pandas/tests/computation/test_eval.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from pandas.errors import (
NumExprClobberingError,
PerformanceWarning,
UndefinedVariableError,
)
import pandas.util._test_decorators as td

Expand Down Expand Up @@ -44,7 +45,6 @@
from pandas.core.computation.ops import (
ARITH_OPS_SYMS,
SPECIAL_CASE_ARITH_OPS_SYMS,
UndefinedVariableError,
_binary_math_ops,
_binary_ops_dict,
_unary_math_ops,
Expand Down
10 changes: 5 additions & 5 deletions pandas/tests/frame/test_query_eval.py
Original file line number Diff line number Diff line change
Expand Up @@ -495,7 +495,7 @@ def test_query_syntax_error(self):
df.query("i - +", engine=engine, parser=parser)

def test_query_scope(self):
from pandas.core.computation.ops import UndefinedVariableError
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These imports might be able to be moved to the top (in a follow up PR would be okay)

from pandas.errors import UndefinedVariableError

engine, parser = self.engine, self.parser
skip_if_no_pandas_parser(parser)
Expand All @@ -522,7 +522,7 @@ def test_query_scope(self):
df.query("@a > b > c", engine=engine, parser=parser)

def test_query_doesnt_pickup_local(self):
from pandas.core.computation.ops import UndefinedVariableError
from pandas.errors import UndefinedVariableError

engine, parser = self.engine, self.parser
n = m = 10
Expand Down Expand Up @@ -618,7 +618,7 @@ def test_nested_scope(self):
tm.assert_frame_equal(result, expected)

def test_nested_raises_on_local_self_reference(self):
from pandas.core.computation.ops import UndefinedVariableError
from pandas.errors import UndefinedVariableError

df = DataFrame(np.random.randn(5, 3))

Expand Down Expand Up @@ -678,7 +678,7 @@ def test_at_inside_string(self):
tm.assert_frame_equal(result, expected)

def test_query_undefined_local(self):
from pandas.core.computation.ops import UndefinedVariableError
from pandas.errors import UndefinedVariableError

engine, parser = self.engine, self.parser
skip_if_no_pandas_parser(parser)
Expand Down Expand Up @@ -838,7 +838,7 @@ def test_date_index_query_with_NaT_duplicates(self):
df.query("index < 20130101 < dates3", engine=engine, parser=parser)

def test_nested_scope(self):
from pandas.core.computation.ops import UndefinedVariableError
from pandas.errors import UndefinedVariableError

engine = self.engine
parser = self.parser
Expand Down
23 changes: 22 additions & 1 deletion pandas/tests/test_errors.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import pytest

from pandas.errors import AbstractMethodError
from pandas.errors import (
AbstractMethodError,
UndefinedVariableError,
)

import pandas as pd

Expand Down Expand Up @@ -48,6 +51,24 @@ def test_catch_oob():
pd.Timestamp("15000101")


@pytest.mark.parametrize(
"is_local",
[
True,
False,
],
)
def test_catch_undefined_variable_error(is_local):
variable_name = "x"
if is_local:
msg = f"local variable '{variable_name}' is not defined"
else:
msg = f"name '{variable_name}' is not defined"

with pytest.raises(UndefinedVariableError, match=msg):
raise UndefinedVariableError(variable_name, is_local)


class Foo:
@classmethod
def classmethod(cls):
Expand Down
4 changes: 2 additions & 2 deletions scripts/pandas_errors_documented.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ def get_defined_errors(content: str) -> set[str]:
for node in ast.walk(ast.parse(content)):
if isinstance(node, ast.ClassDef):
errors.add(node.name)
elif isinstance(node, ast.ImportFrom):
elif isinstance(node, ast.ImportFrom) and node.module != "__future__":
for alias in node.names:
errors.add(alias.name)
return errors
Expand All @@ -41,7 +41,7 @@ def main(argv: Sequence[str] | None = None) -> None:
missing = file_errors.difference(doc_errors)
if missing:
sys.stdout.write(
f"The follow exceptions and/or warnings are not documented "
f"The following exceptions and/or warnings are not documented "
f"in {API_PATH}: {missing}"
)
sys.exit(1)
Expand Down