From 8eea09c849af1446403932cf3da921b048fc479e Mon Sep 17 00:00:00 2001 From: Phillip Cloud Date: Sat, 22 Feb 2014 18:31:18 -0500 Subject: [PATCH] CLN: remove dead code in pandas.computation --- pandas/computation/align.py | 81 +------------ pandas/computation/common.py | 5 +- pandas/computation/engines.py | 1 - pandas/computation/eval.py | 4 +- pandas/computation/expr.py | 9 +- pandas/computation/ops.py | 26 ++-- pandas/computation/pytables.py | 13 -- pandas/computation/scope.py | 8 +- pandas/computation/tests/test_eval.py | 163 ++++++++++++-------------- pandas/io/tests/test_pytables.py | 19 +++ pandas/tests/test_frame.py | 10 ++ 11 files changed, 122 insertions(+), 217 deletions(-) diff --git a/pandas/computation/align.py b/pandas/computation/align.py index 1685f66c15416..2e0845bddf7e2 100644 --- a/pandas/computation/align.py +++ b/pandas/computation/align.py @@ -34,40 +34,6 @@ def _zip_axes_from_type(typ, new_axes): return axes -def _maybe_promote_shape(values, naxes): - # test to see if we have an array else leave since must be a number - if not isinstance(values, np.ndarray): - return values - - ndims = values.ndim - if ndims > naxes: - raise AssertionError('cannot have more dims than axes, ' - '{0} > {1}'.format(ndims, naxes)) - if ndims == naxes: - return values - - ndim, nax = range(ndims), range(naxes) - - axes_slice = [slice(None)] * naxes - - # set difference of numaxes and ndims - slices = list(set(nax) - set(ndim)) - - if ndims == naxes: - if slices: - raise AssertionError('slices should be empty if ndims == naxes ' - '{0}'.format(slices)) - else: - if not slices: - raise AssertionError('slices should NOT be empty if ndim != naxes ' - '{0}'.format(slices)) - - for sl in slices: - axes_slice[sl] = np.newaxis - - return values[tuple(axes_slice)] - - def _any_pandas_objects(terms): """Check a sequence of terms for instances of PandasObject.""" return any(isinstance(term.value, pd.core.generic.PandasObject) @@ -83,12 +49,7 @@ def wrapper(terms): term_values = (term.value for term in terms) - # only scalars or indexes - if all(isinstance(term.value, pd.Index) or term.isscalar for term in - terms): - return _result_type_many(*term_values), None - - # no pandas objects + # we don't have any pandas objects if not _any_pandas_objects(terms): return _result_type_many(*term_values), None @@ -148,44 +109,13 @@ def _align_core(terms): f = partial(ti.reindex_axis, reindexer, axis=axis, copy=False) - # need to fill if we have a bool dtype/array - if (isinstance(ti, (np.ndarray, pd.Series)) - and ti.dtype == object - and pd.lib.is_bool_array(ti.values)): - r = f(fill_value=True) - else: - r = f() - - terms[i].update(r) + terms[i].update(f()) - res = _maybe_promote_shape(terms[i].value.T if transpose else - terms[i].value, naxes) - res = res.T if transpose else res - - try: - v = res.values - except AttributeError: - v = res - terms[i].update(v) + terms[i].update(terms[i].value.values) return typ, _zip_axes_from_type(typ, axes) -def _filter_terms(flat): - # numeric literals - literals = frozenset(filter(lambda x: isinstance(x, Constant), flat)) - - # these are strings which are variable names - names = frozenset(flat) - literals - - # literals are not names and names are not literals, so intersection should - # be empty - if literals & names: - raise ValueError('literals cannot be names and names cannot be ' - 'literals') - return names, literals - - def _align(terms): """Align a set of terms""" try: @@ -231,10 +161,7 @@ def _reconstruct_object(typ, obj, axes, dtype): except AttributeError: pass - try: - res_t = np.result_type(obj.dtype, dtype) - except AttributeError: - res_t = dtype + res_t = np.result_type(obj.dtype, dtype) if (not isinstance(typ, partial) and issubclass(typ, pd.core.generic.PandasObject)): diff --git a/pandas/computation/common.py b/pandas/computation/common.py index 0d5e639032b94..105cc497a4207 100644 --- a/pandas/computation/common.py +++ b/pandas/computation/common.py @@ -16,10 +16,7 @@ def _result_type_many(*arrays_and_dtypes): try: return np.result_type(*arrays_and_dtypes) except ValueError: - # length 0 or length > NPY_MAXARGS both throw a ValueError, so check - # which one we're dealing with - if len(arrays_and_dtypes) == 0: - raise ValueError('at least one array or dtype is required') + # we have > NPY_MAXARGS terms in our expression return reduce(np.result_type, arrays_and_dtypes) diff --git a/pandas/computation/engines.py b/pandas/computation/engines.py index 120e190736516..58b822af546c8 100644 --- a/pandas/computation/engines.py +++ b/pandas/computation/engines.py @@ -126,7 +126,6 @@ def _evaluate(self): raise UndefinedVariableError(msg) - class PythonEngine(AbstractEngine): """Evaluate an expression in Python space. diff --git a/pandas/computation/eval.py b/pandas/computation/eval.py index 46e2292aa0972..82c68fb10e7d6 100644 --- a/pandas/computation/eval.py +++ b/pandas/computation/eval.py @@ -69,8 +69,8 @@ def _check_resolvers(resolvers): for resolver in resolvers: if not hasattr(resolver, '__getitem__'): name = type(resolver).__name__ - raise AttributeError('Resolver of type {0!r} must implement ' - 'the __getitem__ method'.format(name)) + raise TypeError('Resolver of type %r does not implement ' + 'the __getitem__ method' % name) def _check_expression(expr): diff --git a/pandas/computation/expr.py b/pandas/computation/expr.py index 03dc4c981fa9f..1c40dc9930856 100644 --- a/pandas/computation/expr.py +++ b/pandas/computation/expr.py @@ -308,9 +308,6 @@ def visit(self, node, **kwargs): if isinstance(node, string_types): clean = self.preparser(node) node = ast.fix_missing_locations(ast.parse(clean)) - elif not isinstance(node, ast.AST): - raise TypeError("Cannot visit objects of type {0!r}" - "".format(node.__class__.__name__)) method = 'visit_' + node.__class__.__name__ visitor = getattr(self, method) @@ -533,7 +530,7 @@ def visit_Call(self, node, side=None, **kwargs): args = [self.visit(targ).value for targ in node.args] if node.starargs is not None: - args = args + self.visit(node.starargs).value + args += self.visit(node.starargs).value keywords = {} for key in node.keywords: @@ -651,10 +648,6 @@ def parse(self): """Parse an expression""" return self._visitor.visit(self.expr) - def align(self): - """align a set of Terms""" - return self.terms.align(self.env) - @property def names(self): """Get the names in an expression""" diff --git a/pandas/computation/ops.py b/pandas/computation/ops.py index 93c10fc42ee36..041ab77bb61f4 100644 --- a/pandas/computation/ops.py +++ b/pandas/computation/ops.py @@ -29,13 +29,12 @@ class UndefinedVariableError(NameError): """NameError subclass for local variables.""" - def __init__(self, *args): - msg = 'name {0!r} is not defined' - subbed = args[0].replace(_LOCAL_TAG, '') - if subbed != args[0]: - subbed = '@' + subbed + def __init__(self, name, is_local): + if is_local: msg = 'local variable {0!r} is not defined' - super(UndefinedVariableError, self).__init__(msg.format(subbed)) + else: + msg = 'name {0!r} is not defined' + super(UndefinedVariableError, self).__init__(msg.format(name)) class Term(StringMixin): @@ -73,11 +72,6 @@ def _resolve_name(self): res = self.env.resolve(self.local_name, is_local=self.is_local) self.update(res) - if res is None: - if not isinstance(key, string_types): - return key - raise UndefinedVariableError(key) - if hasattr(res, 'ndim') and res.ndim > 2: raise NotImplementedError("N-dimensional objects, where N > 2," " are not supported with eval") @@ -97,10 +91,7 @@ def update(self, value): # if it's a variable name (otherwise a constant) if isinstance(key, string_types): - try: - self.env.swapkey(self.local_name, key, new_value=value) - except KeyError: - raise UndefinedVariableError(key) + self.env.swapkey(self.local_name, key, new_value=value) self.value = value @@ -156,10 +147,7 @@ def name(self, new_name): @property def ndim(self): - try: - return self._value.ndim - except AttributeError: - return 0 + return self._value.ndim class Constant(Term): diff --git a/pandas/computation/pytables.py b/pandas/computation/pytables.py index b995909ed15ad..8fc842d958075 100644 --- a/pandas/computation/pytables.py +++ b/pandas/computation/pytables.py @@ -67,10 +67,6 @@ def __init__(self, value, env, side=None, encoding=None): def _resolve_name(self): return self._name - @property - def name(self): - return self._value - class BinOp(ops.BinOp): @@ -233,9 +229,6 @@ def format(self): def evaluate(self): - if not isinstance(self.lhs, string_types): - return self - if not self.is_valid: raise ValueError("query term is not valid [%s]" % self) @@ -307,9 +300,6 @@ def format(self): def evaluate(self): - if not isinstance(self.lhs, string_types): - return self - if not self.is_valid: raise ValueError("query term is not valid [%s]" % self) @@ -390,9 +380,6 @@ def visit_UnaryOp(self, node, **kwargs): elif isinstance(node.op, ast.UAdd): raise NotImplementedError('Unary addition not supported') - def visit_USub(self, node, **kwargs): - return self.const_type(-self.visit(node.operand).value, self.env) - def visit_Index(self, node, **kwargs): return self.visit(node.value).value diff --git a/pandas/computation/scope.py b/pandas/computation/scope.py index eaeba86a0e946..004d8d39d5e82 100644 --- a/pandas/computation/scope.py +++ b/pandas/computation/scope.py @@ -186,7 +186,7 @@ def resolve(self, key, is_local): # e.g., df[df > 0] return self.temps[key] except KeyError: - raise UndefinedVariableError(key) + raise UndefinedVariableError(key, is_local) def swapkey(self, old_key, new_key, new_value=None): """Replace a variable name, with a potentially new value. @@ -209,12 +209,8 @@ def swapkey(self, old_key, new_key, new_value=None): for mapping in maps: if old_key in mapping: - if new_value is None: - mapping[new_key] = mapping.pop(old_key) - else: - mapping[new_key] = new_value + mapping[new_key] = new_value return - raise KeyError(old_key) def _get_vars(self, stack, scopes): """Get specifically scoped variables from a list of stack frames. diff --git a/pandas/computation/tests/test_eval.py b/pandas/computation/tests/test_eval.py index 099e8b0412134..0ce93c48d32f5 100644 --- a/pandas/computation/tests/test_eval.py +++ b/pandas/computation/tests/test_eval.py @@ -18,7 +18,7 @@ from pandas.util.testing import makeCustomDataframe as mkdf from pandas.computation import pytables -from pandas.computation.engines import _engines +from pandas.computation.engines import _engines, NumExprClobberingError from pandas.computation.expr import PythonExprVisitor, PandasExprVisitor from pandas.computation.ops import (_binary_ops_dict, _special_case_arith_ops_syms, @@ -73,23 +73,6 @@ def _bool_and_frame(lhs, rhs): return isinstance(lhs, bool) and isinstance(rhs, pd.core.generic.NDFrame) -def skip_incompatible_operand(f): - @functools.wraps(f) - def wrapper(self, lhs, arith1, rhs, *args, **kwargs): - if _series_and_2d_ndarray(lhs, rhs): - self.assertRaises(Exception, pd.eval, 'lhs {0} rhs'.format(arith1), - local_dict={'lhs': lhs, 'rhs': rhs}, - engine=self.engine, parser=self.parser) - elif (np.isscalar(lhs) and np.isscalar(rhs) and arith1 in - _bool_ops_syms): - with tm.assertRaises(NotImplementedError): - pd.eval('lhs {0} rhs'.format(arith1), engine=self.engine, - parser=self.parser) - else: - f(self, lhs, arith1, rhs, *args, **kwargs) - return wrapper - - def _is_py3_complex_incompat(result, expected): return (PY3 and isinstance(expected, (complex, np.complexfloating)) and np.isnan(result)) @@ -199,7 +182,6 @@ def test_compound_invert_op(self): @slow def test_chained_cmp_op(self): mids = self.lhses - # tuple(set(self.cmp_ops) - set(['==', '!=', '<=', '>='])) cmp_ops = '<', '>' for lhs, cmp1, mid, cmp2, rhs in product(self.lhses, cmp_ops, mids, cmp_ops, self.rhses): @@ -213,26 +195,11 @@ def check_complex_cmp_op(self, lhs, cmp1, rhs, binop, cmp2): scalar_with_in_notin = (np.isscalar(rhs) and (cmp1 in skip_these or cmp2 in skip_these)) if scalar_with_in_notin: + with tm.assertRaises(TypeError): + pd.eval(ex, engine=self.engine, parser=self.parser) self.assertRaises(TypeError, pd.eval, ex, engine=self.engine, parser=self.parser, local_dict={'lhs': lhs, 'rhs': rhs}) - elif (_series_and_frame(lhs, rhs) and (cmp1 in - _series_frame_incompatible or - cmp2 in _series_frame_incompatible)): - self.assertRaises(TypeError, pd.eval, ex, - local_dict={'lhs': lhs, 'rhs': rhs}, - engine=self.engine, parser=self.parser) - elif _bool_and_frame(lhs, rhs): - self.assertRaises(TypeError, _eval_single_bin, lhs, '&', - rhs, self.engine) - self.assertRaises(TypeError, pd.eval, ex, - local_dict={'lhs': lhs, 'rhs': rhs}, - engine=self.engine, parser=self.parser) - elif (np.isscalar(lhs) and np.isnan(lhs) and - not np.isscalar(rhs) and (cmp1 in skip_these or cmp2 in - skip_these)): - with tm.assertRaises(TypeError): - _eval_single_bin(lhs, binop, rhs, self.engine) else: lhs_new = _eval_single_bin(lhs, cmp1, rhs, self.engine) rhs_new = _eval_single_bin(lhs, cmp2, rhs, self.engine) @@ -249,51 +216,17 @@ def check_complex_cmp_op(self, lhs, cmp1, rhs, binop, cmp2): # except AssertionError: #import ipdb; ipdb.set_trace() # raise - elif (np.isscalar(lhs_new) and np.isnan(lhs_new) and - not np.isscalar(rhs_new) and binop in skip_these): - with tm.assertRaises(TypeError): - _eval_single_bin(lhs_new, binop, rhs_new, self.engine) else: expected = _eval_single_bin( lhs_new, binop, rhs_new, self.engine) result = pd.eval(ex, engine=self.engine, parser=self.parser) assert_array_equal(result, expected) - @skip_incompatible_operand def check_chained_cmp_op(self, lhs, cmp1, mid, cmp2, rhs): skip_these = _scalar_skip def check_operands(left, right, cmp_op): - if (np.isscalar(left) and np.isnan(left) and not np.isscalar(right) - and cmp_op in skip_these): - ex = 'left {0} right'.format(cmp_op) - with tm.assertRaises(ValueError): - pd.eval(ex, engine=self.engine, parser=self.parser) - return - if (np.isscalar(left) and np.isscalar(right) and - cmp_op in _bool_ops_syms): - ex1 = 'lhs {0} mid {1} rhs'.format(cmp1, cmp2) - ex2 = 'lhs {0} mid and mid {1} rhs'.format(cmp1, cmp2) - ex3 = '(lhs {0} mid) & (mid {1} rhs)'.format(cmp1, cmp2) - for ex in (ex1, ex2, ex3): - with tm.assertRaises(NotImplementedError): - pd.eval(ex, engine=self.engine, parser=self.parser) - return - if (np.isscalar(right) and not np.isscalar(left) and cmp_op in - skip_these): - self.assertRaises(Exception, _eval_single_bin, left, cmp_op, - right, self.engine) - elif _series_and_2d_ndarray(right, left): - self.assertRaises(Exception, _eval_single_bin, right, cmp_op, - left, self.engine) - elif (np.isscalar(right) and np.isscalar(left) and cmp_op in - skip_these): - self.assertRaises(Exception, _eval_single_bin, right, cmp_op, - left, self.engine) - else: - new = _eval_single_bin(left, cmp_op, right, self.engine) - return new - return + return _eval_single_bin(left, cmp_op, right, self.engine) lhs_new = check_operands(lhs, mid, cmp1) rhs_new = check_operands(mid, rhs, cmp2) @@ -309,7 +242,6 @@ def check_operands(left, right, cmp_op): parser=self.parser) assert_array_equal(result, expected) - @skip_incompatible_operand def check_simple_cmp_op(self, lhs, cmp1, rhs): ex = 'lhs {0} rhs'.format(cmp1) if cmp1 in ('in', 'not in') and not com.is_list_like(rhs): @@ -321,7 +253,6 @@ def check_simple_cmp_op(self, lhs, cmp1, rhs): result = pd.eval(ex, engine=self.engine, parser=self.parser) assert_array_equal(result, expected) - @skip_incompatible_operand def check_binary_arith_op(self, lhs, arith1, rhs): ex = 'lhs {0} rhs'.format(arith1) result = pd.eval(ex, engine=self.engine, parser=self.parser) @@ -344,9 +275,8 @@ def check_alignment(self, result, nlhs, ghs, op): expected = self.ne.evaluate('nlhs {0} ghs'.format(op)) assert_array_equal(result, expected) - # the following 3 tests require special casing + # modulus, pow, and floor division require special casing - @skip_incompatible_operand def check_modulus(self, lhs, arith1, rhs): ex = 'lhs {0} rhs'.format(arith1) result = pd.eval(ex, engine=self.engine, parser=self.parser) @@ -355,7 +285,6 @@ def check_modulus(self, lhs, arith1, rhs): expected = self.ne.evaluate('expected {0} rhs'.format(arith1)) assert_allclose(result, expected) - @skip_incompatible_operand def check_floor_division(self, lhs, arith1, rhs): ex = 'lhs {0} rhs'.format(arith1) @@ -389,7 +318,6 @@ def get_expected_pow_result(self, lhs, rhs): raise return expected - @skip_incompatible_operand def check_pow(self, lhs, arith1, rhs): ex = 'lhs {0} rhs'.format(arith1) expected = self.get_expected_pow_result(lhs, rhs) @@ -408,7 +336,6 @@ def check_pow(self, lhs, arith1, rhs): self.get_expected_pow_result(lhs, rhs), rhs) assert_allclose(result, expected) - @skip_incompatible_operand def check_single_invert_op(self, lhs, cmp1, rhs): # simple for el in (lhs, rhs): @@ -425,7 +352,6 @@ def check_single_invert_op(self, lhs, cmp1, rhs): assert_array_equal(result, pd.eval('~elb', engine=engine, parser=self.parser)) - @skip_incompatible_operand def check_compound_invert_op(self, lhs, cmp1, rhs): skip_these = 'in', 'not in' ex = '~(lhs {0} rhs)'.format(cmp1) @@ -434,10 +360,6 @@ def check_compound_invert_op(self, lhs, cmp1, rhs): self.assertRaises(TypeError, pd.eval, ex, engine=self.engine, parser=self.parser, local_dict={'lhs': lhs, 'rhs': rhs}) - elif (np.isscalar(lhs) and np.isnan(lhs) and not np.isscalar(rhs) - and cmp1 in skip_these): - with tm.assertRaises(ValueError): - pd.eval(ex, engine=self.engine, parser=self.parser) else: # compound if np.isscalar(lhs) and np.isscalar(rhs): @@ -734,13 +656,14 @@ def setUpClass(cls): cls.engine = 'python' cls.parser = 'python' - @skip_incompatible_operand def check_modulus(self, lhs, arith1, rhs): ex = 'lhs {0} rhs'.format(arith1) - result = pd.eval(ex, engine=self.engine) + result = pd.eval(ex, engine=self.engine, parser=self.parser) + expected = lhs % rhs assert_allclose(result, expected) - expected = eval('expected {0} rhs'.format(arith1)) + + expected = _eval_single_bin(expected, arith1, rhs, self.engine) assert_allclose(result, expected) def check_alignment(self, result, nlhs, ghs, op): @@ -1234,7 +1157,9 @@ def f(): a = 1 old_a = df.a.copy() df.eval('a = a + b') - assert_frame_equal(old_a + df.b, df.a) + assert_series_equal(old_a + df.b, df.a) + + f() # multiple assignment df = orig_df.copy() @@ -1575,6 +1500,70 @@ def test_invalid_local_variable_reference(): yield check_invalid_local_variable_reference, engine, parser +def check_numexpr_builtin_raises(engine, parser): + tm.skip_if_no_ne(engine) + sin, dotted_line = 1, 2 + if engine == 'numexpr': + with tm.assertRaisesRegexp(NumExprClobberingError, + 'Variables in expression .+'): + pd.eval('sin + dotted_line', engine=engine, parser=parser) + else: + res = pd.eval('sin + dotted_line', engine=engine, parser=parser) + tm.assert_equal(res, sin + dotted_line) + + +def test_numexpr_builtin_raises(): + for engine, parser in ENGINES_PARSERS: + yield check_numexpr_builtin_raises, engine, parser + + +def check_bad_resolver_raises(engine, parser): + tm.skip_if_no_ne(engine) + cannot_resolve = 42, 3.0 + with tm.assertRaisesRegexp(TypeError, 'Resolver of type .+'): + pd.eval('1 + 2', resolvers=cannot_resolve, engine=engine, + parser=parser) + + +def test_bad_resolver_raises(): + for engine, parser in ENGINES_PARSERS: + yield check_bad_resolver_raises, engine, parser + + +def check_more_than_one_expression_raises(engine, parser): + tm.skip_if_no_ne(engine) + with tm.assertRaisesRegexp(SyntaxError, + 'only a single expression is allowed'): + pd.eval('1 + 1; 2 + 2', engine=engine, parser=parser) + + +def test_more_than_one_expression_raises(): + for engine, parser in ENGINES_PARSERS: + yield check_more_than_one_expression_raises, engine, parser + + +def check_bool_ops_fails_on_scalars(gen, lhs, cmp, rhs, engine, parser): + tm.skip_if_no_ne(engine) + mid = gen[type(lhs)]() + ex1 = 'lhs {0} mid {1} rhs'.format(cmp, cmp) + ex2 = 'lhs {0} mid and mid {1} rhs'.format(cmp, cmp) + ex3 = '(lhs {0} mid) & (mid {1} rhs)'.format(cmp, cmp) + for ex in (ex1, ex2, ex3): + with tm.assertRaises(NotImplementedError): + pd.eval(ex, engine=engine, parser=parser) + + +def test_bool_ops_fails_on_scalars(): + _bool_ops_syms = 'and', 'or' + dtypes = int, float + gen = {int: lambda : np.random.randint(10), float: np.random.randn} + for engine, parser, dtype1, cmp, dtype2 in product(_engines, expr._parsers, + dtypes, _bool_ops_syms, + dtypes): + yield (check_bool_ops_fails_on_scalars, gen, gen[dtype1](), cmp, + gen[dtype2](), engine, parser) + + if __name__ == '__main__': nose.runmodule(argv=[__file__, '-vvs', '-x', '--pdb', '--pdb-failure'], exit=False) diff --git a/pandas/io/tests/test_pytables.py b/pandas/io/tests/test_pytables.py index 0f2d674f9efd4..7b9b9d50f2178 100644 --- a/pandas/io/tests/test_pytables.py +++ b/pandas/io/tests/test_pytables.py @@ -2371,8 +2371,11 @@ def test_terms(self): wp = tm.makePanel() p4d = tm.makePanel4D() + wpneg = Panel.fromDict({-1: tm.makeDataFrame(), 0: tm.makeDataFrame(), + 1: tm.makeDataFrame()}) store.put('wp', wp, table=True) store.put('p4d', p4d, table=True) + store.put('wpneg', wpneg, table=True) # panel result = store.select('wp', [Term( @@ -2433,6 +2436,18 @@ def test_terms(self): for t in terms: store.select('p4d', t) + with tm.assertRaisesRegexp(TypeError, 'Only named functions are supported'): + store.select('wp', Term('major_axis == (lambda x: x)("20130101")')) + + # check USub node parsing + res = store.select('wpneg', Term('items == -1')) + expected = Panel({-1: wpneg[-1]}) + tm.assert_panel_equal(res, expected) + + with tm.assertRaisesRegexp(NotImplementedError, + 'Unary addition not supported'): + store.select('wpneg', Term('items == +1')) + def test_term_compat(self): with ensure_clean_store(self.path) as store: @@ -3829,6 +3844,10 @@ def test_select_filter_corner(self): result = store.select('frame', [crit]) tm.assert_frame_equal(result, df.ix[:, df.columns[:75]]) + crit = Term('columns=df.columns[:75:2]') + result = store.select('frame', [crit]) + tm.assert_frame_equal(result, df.ix[:, df.columns[:75:2]]) + def _check_roundtrip(self, obj, comparator, compression=False, **kwargs): options = {} diff --git a/pandas/tests/test_frame.py b/pandas/tests/test_frame.py index e83c22badbc04..3c39d610c1b88 100644 --- a/pandas/tests/test_frame.py +++ b/pandas/tests/test_frame.py @@ -12796,6 +12796,16 @@ def test_at_inside_string(self): expected = df[df.a == "@c"] tm.assert_frame_equal(result, expected) + def test_query_undefined_local(self): + from pandas.computation.ops import UndefinedVariableError + engine, parser = self.engine, self.parser + skip_if_no_pandas_parser(parser) + df = DataFrame(np.random.rand(10, 2), columns=list('ab')) + with tm.assertRaisesRegexp(UndefinedVariableError, + "local variable 'c' is not defined"): + df.query('a == @c', engine=engine, parser=parser) + + class TestDataFrameQueryNumExprPython(TestDataFrameQueryNumExprPandas): @classmethod