Skip to content

Commit f40203c

Browse files
authored
ENH: Move UndefinedVariableError to error/__init__.py per GH27656 (#47338)
1 parent 87da500 commit f40203c

File tree

11 files changed

+63
-32
lines changed

11 files changed

+63
-32
lines changed

doc/source/reference/testing.rst

+1
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ Exceptions and warnings
4545
errors.SettingWithCopyError
4646
errors.SettingWithCopyWarning
4747
errors.SpecificationError
48+
errors.UndefinedVariableError
4849
errors.UnsortedIndexError
4950
errors.UnsupportedFunctionCall
5051

doc/source/whatsnew/v1.5.0.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ Other enhancements
152152
- A :class:`errors.PerformanceWarning` is now thrown when using ``string[pyarrow]`` dtype with methods that don't dispatch to ``pyarrow.compute`` methods (:issue:`42613`)
153153
- 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`)
154154
- ``times`` argument in :class:`.ExponentialMovingWindow` now accepts ``np.timedelta64`` (:issue:`47003`)
155-
- :class:`DataError`, :class:`SpecificationError`, :class:`SettingWithCopyError`, :class:`SettingWithCopyWarning`, and :class:`NumExprClobberingError` are now exposed in ``pandas.errors`` (:issue:`27656`)
155+
- :class:`DataError`, :class:`SpecificationError`, :class:`SettingWithCopyError`, :class:`SettingWithCopyWarning`, :class:`NumExprClobberingError`, :class:`UndefinedVariableError` are now exposed in ``pandas.errors`` (:issue:`27656`)
156156
- Added ``check_like`` argument to :func:`testing.assert_series_equal` (:issue:`47247`)
157157

158158
.. ---------------------------------------------------------------------------

pandas/core/computation/expr.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import numpy as np
1919

2020
from pandas.compat import PY39
21+
from pandas.errors import UndefinedVariableError
2122

2223
import pandas.core.common as com
2324
from pandas.core.computation.ops import (
@@ -35,7 +36,6 @@
3536
Op,
3637
Term,
3738
UnaryOp,
38-
UndefinedVariableError,
3939
is_term,
4040
)
4141
from pandas.core.computation.parsing import (

pandas/core/computation/ops.py

-14
Original file line numberDiff line numberDiff line change
@@ -65,20 +65,6 @@
6565
LOCAL_TAG = "__pd_eval_local_"
6666

6767

68-
class UndefinedVariableError(NameError):
69-
"""
70-
NameError subclass for local variables.
71-
"""
72-
73-
def __init__(self, name: str, is_local: bool | None = None) -> None:
74-
base_msg = f"{repr(name)} is not defined"
75-
if is_local:
76-
msg = f"local variable {base_msg}"
77-
else:
78-
msg = f"name {base_msg}"
79-
super().__init__(msg)
80-
81-
8268
class Term:
8369
def __new__(cls, name, env, side=None, encoding=None):
8470
klass = Constant if not isinstance(name, str) else cls

pandas/core/computation/pytables.py

+2-4
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
)
1414
from pandas._typing import npt
1515
from pandas.compat.chainmap import DeepChainMap
16+
from pandas.errors import UndefinedVariableError
1617

1718
from pandas.core.dtypes.common import is_list_like
1819

@@ -24,10 +25,7 @@
2425
)
2526
from pandas.core.computation.common import ensure_decoded
2627
from pandas.core.computation.expr import BaseExprVisitor
27-
from pandas.core.computation.ops import (
28-
UndefinedVariableError,
29-
is_term,
30-
)
28+
from pandas.core.computation.ops import is_term
3129
from pandas.core.construction import extract_array
3230
from pandas.core.indexes.base import Index
3331

pandas/core/computation/scope.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
from pandas._libs.tslibs import Timestamp
1717
from pandas.compat.chainmap import DeepChainMap
18+
from pandas.errors import UndefinedVariableError
1819

1920

2021
def ensure_scope(
@@ -207,9 +208,6 @@ def resolve(self, key: str, is_local: bool):
207208
# e.g., df[df > 0]
208209
return self.temps[key]
209210
except KeyError as err:
210-
# runtime import because ops imports from scope
211-
from pandas.core.computation.ops import UndefinedVariableError
212-
213211
raise UndefinedVariableError(key, is_local) from err
214212

215213
def swapkey(self, old_key: str, new_key: str, new_value=None) -> None:

pandas/errors/__init__.py

+27
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""
22
Expose public exceptions & warnings
33
"""
4+
from __future__ import annotations
45

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

@@ -326,3 +327,29 @@ class NumExprClobberingError(NameError):
326327
>>> pd.eval("sin + a", engine='numexpr') # doctest: +SKIP
327328
... # NumExprClobberingError: Variables in expression "(sin) + (a)" overlap...
328329
"""
330+
331+
332+
class UndefinedVariableError(NameError):
333+
"""
334+
Exception is raised when trying to use an undefined variable name in a method
335+
like query or eval. It will also specific whether the undefined variable is
336+
local or not.
337+
338+
Examples
339+
--------
340+
>>> df = pd.DataFrame({'A': [1, 1, 1]})
341+
>>> df.query("A > x") # doctest: +SKIP
342+
... # UndefinedVariableError: name 'x' is not defined
343+
>>> df.query("A > @y") # doctest: +SKIP
344+
... # UndefinedVariableError: local variable 'y' is not defined
345+
>>> pd.eval('x + 1') # doctest: +SKIP
346+
... # UndefinedVariableError: name 'x' is not defined
347+
"""
348+
349+
def __init__(self, name: str, is_local: bool | None = None) -> None:
350+
base_msg = f"{repr(name)} is not defined"
351+
if is_local:
352+
msg = f"local variable {base_msg}"
353+
else:
354+
msg = f"name {base_msg}"
355+
super().__init__(msg)

pandas/tests/computation/test_eval.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from pandas.errors import (
1313
NumExprClobberingError,
1414
PerformanceWarning,
15+
UndefinedVariableError,
1516
)
1617
import pandas.util._test_decorators as td
1718

@@ -44,7 +45,6 @@
4445
from pandas.core.computation.ops import (
4546
ARITH_OPS_SYMS,
4647
SPECIAL_CASE_ARITH_OPS_SYMS,
47-
UndefinedVariableError,
4848
_binary_math_ops,
4949
_binary_ops_dict,
5050
_unary_math_ops,

pandas/tests/frame/test_query_eval.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -495,7 +495,7 @@ def test_query_syntax_error(self):
495495
df.query("i - +", engine=engine, parser=parser)
496496

497497
def test_query_scope(self):
498-
from pandas.core.computation.ops import UndefinedVariableError
498+
from pandas.errors import UndefinedVariableError
499499

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

524524
def test_query_doesnt_pickup_local(self):
525-
from pandas.core.computation.ops import UndefinedVariableError
525+
from pandas.errors import UndefinedVariableError
526526

527527
engine, parser = self.engine, self.parser
528528
n = m = 10
@@ -618,7 +618,7 @@ def test_nested_scope(self):
618618
tm.assert_frame_equal(result, expected)
619619

620620
def test_nested_raises_on_local_self_reference(self):
621-
from pandas.core.computation.ops import UndefinedVariableError
621+
from pandas.errors import UndefinedVariableError
622622

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

@@ -678,7 +678,7 @@ def test_at_inside_string(self):
678678
tm.assert_frame_equal(result, expected)
679679

680680
def test_query_undefined_local(self):
681-
from pandas.core.computation.ops import UndefinedVariableError
681+
from pandas.errors import UndefinedVariableError
682682

683683
engine, parser = self.engine, self.parser
684684
skip_if_no_pandas_parser(parser)
@@ -838,7 +838,7 @@ def test_date_index_query_with_NaT_duplicates(self):
838838
df.query("index < 20130101 < dates3", engine=engine, parser=parser)
839839

840840
def test_nested_scope(self):
841-
from pandas.core.computation.ops import UndefinedVariableError
841+
from pandas.errors import UndefinedVariableError
842842

843843
engine = self.engine
844844
parser = self.parser

pandas/tests/test_errors.py

+22-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import pytest
22

3-
from pandas.errors import AbstractMethodError
3+
from pandas.errors import (
4+
AbstractMethodError,
5+
UndefinedVariableError,
6+
)
47

58
import pandas as pd
69

@@ -48,6 +51,24 @@ def test_catch_oob():
4851
pd.Timestamp("15000101")
4952

5053

54+
@pytest.mark.parametrize(
55+
"is_local",
56+
[
57+
True,
58+
False,
59+
],
60+
)
61+
def test_catch_undefined_variable_error(is_local):
62+
variable_name = "x"
63+
if is_local:
64+
msg = f"local variable '{variable_name}' is not defined"
65+
else:
66+
msg = f"name '{variable_name}' is not defined"
67+
68+
with pytest.raises(UndefinedVariableError, match=msg):
69+
raise UndefinedVariableError(variable_name, is_local)
70+
71+
5172
class Foo:
5273
@classmethod
5374
def classmethod(cls):

scripts/pandas_errors_documented.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ def get_defined_errors(content: str) -> set[str]:
2222
for node in ast.walk(ast.parse(content)):
2323
if isinstance(node, ast.ClassDef):
2424
errors.add(node.name)
25-
elif isinstance(node, ast.ImportFrom):
25+
elif isinstance(node, ast.ImportFrom) and node.module != "__future__":
2626
for alias in node.names:
2727
errors.add(alias.name)
2828
return errors
@@ -41,7 +41,7 @@ def main(argv: Sequence[str] | None = None) -> None:
4141
missing = file_errors.difference(doc_errors)
4242
if missing:
4343
sys.stdout.write(
44-
f"The follow exceptions and/or warnings are not documented "
44+
f"The following exceptions and/or warnings are not documented "
4545
f"in {API_PATH}: {missing}"
4646
)
4747
sys.exit(1)

0 commit comments

Comments
 (0)