Repository: dask/dask

Issue:
DataFrame .query with local_dict fails
This bug appears as soon as query variables are used in `.query`:

# Prelude
```python
import pandas as pd
import dask.dataframe as dd
df = pd.DataFrame({'a': [1, 2, 3]})
ddf = dd.from_pandas(df, npartitions=1)
```

# This works

```python
df.query('a > @value', local_dict={'value': 2}).head()
```
returns just fine

# This fails

```python
ddf.query('a > @value', local_dict={'value': 2}).head()
```

with

```
~/jupyter-notes/env/lib/python3.6/site-packages/dask/dataframe/core.py in head(self, n, npartitions, compute)
    819 
    820         if compute:
--> 821             result = result.compute()
    822         return result
    823 

~/jupyter-notes/env/lib/python3.6/site-packages/dask/base.py in compute(self, **kwargs)
    133         dask.base.compute
    134         """
--> 135         (result,) = compute(self, traverse=False, **kwargs)
    136         return result
    137 

~/jupyter-notes/env/lib/python3.6/site-packages/dask/base.py in compute(*args, **kwargs)
    331     postcomputes = [a.__dask_postcompute__() if is_dask_collection(a)
    332                     else (None, a) for a in args]
--> 333     results = get(dsk, keys, **kwargs)
    334     results_iter = iter(results)
    335     return tuple(a if f is None else f(next(results_iter), *a)

~/jupyter-notes/env/lib/python3.6/site-packages/dask/threaded.py in get(dsk, result, cache, num_workers, **kwargs)
     73     results = get_async(pool.apply_async, len(pool._pool), dsk, result,
     74                         cache=cache, get_id=_thread_get_id,
---> 75                         pack_exception=pack_exception, **kwargs)
     76 
     77     # Cleanup pools associated to dead threads

~/jupyter-notes/env/lib/python3.6/site-packages/dask/local.py in get_async(apply_async, num_workers, dsk, result, cache, get_id, rerun_exceptions_locally, pack_exception, raise_exception, callbacks, dumps, loads, **kwargs)
    519                         _execute_task(task, data)  # Re-execute locally
    520                     else:
--> 521                         raise_exception(exc, tb)
    522                 res, worker_id = loads(res_info)
    523                 state['cache'][key] = res

~/jupyter-notes/env/lib/python3.6/site-packages/dask/compatibility.py in reraise(exc, tb)
     58         if exc.__traceback__ is not tb:
     59             raise exc.with_traceback(tb)
---> 60         raise exc
     61 
     62 else:

~/jupyter-notes/env/lib/python3.6/site-packages/dask/local.py in execute_task(key, task_info, dumps, loads, get_id, pack_exception)
    288     try:
    289         task, data = loads(task_info)
--> 290         result = _execute_task(task, data)
    291         id = get_id()
    292         result = dumps((result, id))

~/jupyter-notes/env/lib/python3.6/site-packages/dask/local.py in _execute_task(arg, cache, dsk)
    269         func, args = arg[0], arg[1:]
    270         args2 = [_execute_task(a, cache) for a in args]
--> 271         return func(*args2)
    272     elif not ishashable(arg):
    273         return arg

~/jupyter-notes/env/lib/python3.6/site-packages/dask/compatibility.py in apply(func, args, kwargs)
     47             return func(*args, **kwargs)
     48         else:
---> 49             return func(*args)
     50     range = range
     51     reduce = functools.reduce

~/jupyter-notes/env/lib/python3.6/site-packages/dask/utils.py in __call__(self, obj, *args, **kwargs)
    685 
    686     def __call__(self, obj, *args, **kwargs):
--> 687         return getattr(obj, self.method)(*args, **kwargs)
    688 
    689     def __reduce__(self):

AttributeError: 'tuple' object has no attribute 'query'
```


Discussion:

Resolving patch:
diff --git a/dask/dataframe/core.py b/dask/dataframe/core.py
--- a/dask/dataframe/core.py
+++ b/dask/dataframe/core.py
@@ -2519,19 +2519,7 @@ def query(self, expr, **kwargs):
         --------
         pandas.DataFrame.query
         """
-        name = 'query-%s' % tokenize(self, expr)
-        if kwargs:
-            name = name + '--' + tokenize(kwargs)
-            dsk = dict(((name, i), (apply, M.query,
-                                    ((self._name, i), (expr,), kwargs)))
-                       for i in range(self.npartitions))
-        else:
-            dsk = dict(((name, i), (M.query, (self._name, i), expr))
-                       for i in range(self.npartitions))
-
-        meta = self._meta.query(expr, **kwargs)
-        return new_dd_object(merge(dsk, self.dask), name,
-                             meta, self.divisions)
+        return self.map_partitions(M.query, expr, **kwargs)
 
     @derived_from(pd.DataFrame)
     def eval(self, expr, inplace=None, **kwargs):


Test patch:
diff --git a/dask/dataframe/tests/test_dataframe.py b/dask/dataframe/tests/test_dataframe.py
--- a/dask/dataframe/tests/test_dataframe.py
+++ b/dask/dataframe/tests/test_dataframe.py
@@ -10,7 +10,7 @@
 import dask
 import dask.dataframe as dd
 from dask.base import compute_as_if_collection
-from dask.utils import ignoring, put_lines
+from dask.utils import put_lines
 
 from dask.dataframe.core import repartition_divisions, aca, _concat, Scalar
 from dask.dataframe import methods
@@ -1504,29 +1504,34 @@ def test_empty_max():
 
 
 def test_query():
+    pytest.importorskip('numexpr')
+
     df = pd.DataFrame({'x': [1, 2, 3, 4], 'y': [5, 6, 7, 8]})
-    a = dd.from_pandas(df, npartitions=2)
-    q = a.query('x**2 > y')
-    with ignoring(ImportError):
-        assert_eq(q, df.query('x**2 > y'))
+    ddf = dd.from_pandas(df, npartitions=2)
+    assert_eq(ddf.query('x**2 > y'),
+              df.query('x**2 > y'))
+    assert_eq(ddf.query('x**2 > @value', local_dict={'value': 4}),
+              df.query('x**2 > @value', local_dict={'value': 4}))
 
 
 def test_eval():
+    pytest.importorskip('numexpr')
+
     p = pd.DataFrame({'x': [1, 2, 3, 4], 'y': [5, 6, 7, 8]})
     d = dd.from_pandas(p, npartitions=2)
-    with ignoring(ImportError):
-        assert_eq(p.eval('x + y'), d.eval('x + y'))
-        assert_eq(p.eval('z = x + y', inplace=False),
-                  d.eval('z = x + y', inplace=False))
-        with pytest.raises(NotImplementedError):
-            d.eval('z = x + y', inplace=True)
-
-        # catch FutureWarning from pandas about assignment in eval
-        with pytest.warns(None):
-            if PANDAS_VERSION < '0.21.0':
-                if p.eval('z = x + y', inplace=None) is None:
-                    with pytest.raises(NotImplementedError):
-                        d.eval('z = x + y', inplace=None)
+
+    assert_eq(p.eval('x + y'), d.eval('x + y'))
+    assert_eq(p.eval('z = x + y', inplace=False),
+              d.eval('z = x + y', inplace=False))
+    with pytest.raises(NotImplementedError):
+        d.eval('z = x + y', inplace=True)
+
+    # catch FutureWarning from pandas about assignment in eval
+    with pytest.warns(None):
+        if PANDAS_VERSION < '0.21.0':
+            if p.eval('z = x + y', inplace=None) is None:
+                with pytest.raises(NotImplementedError):
+                    d.eval('z = x + y', inplace=None)
 
 
 @pytest.mark.parametrize('include, exclude', [