Repository: pandas-dev/pandas

Issue:
Arithmetic operation between Series and `bool` fails
In the current master, I am no longer able to add or subtract `bool` scalars from Series. For example:

```
In [63]: (covariates.isnull().sum(axis=1).astype(bool))
Out[63]:
case_id
A0001      False
A0002      False
A0003      False
A0004      False
A0005      False
A0006      False
A0007      False
A0008      False
A0009      False
A0010      False
A0011      False
A0012      False
A0013      False
A0014      False
A0015      False
...
D3760       True
D3761      False
D3762      False
D3763      False
D3764      False
D3765      False
D3766      False
D3767      False
D3768      False
D3769      False
D3770      False
D3771      False
D3772      False
D3773      False
D3774      False
Length: 3169, dtype: bool

In [65]: (covariates.isnull().sum(axis=1).astype(bool)).value_counts()
Out[65]:
False    2677
True      492
dtype: int64
```

When I subtract `True` to flip the trues and falses, I get:

```
In [66]: (covariates.isnull().sum(axis=1).astype(bool)) - True
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-66-6db12b7f95bf> in <module>()
----> 1 (covariates.isnull().sum(axis=1).astype(bool)) - True

/usr/local/Cellar/python/2.7.6/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site-packages/pandas-0.13.1_780_gdc098d8-py2.7-macosx-10.9-x86_64.egg/pandas/core/ops.pyc in wrapper(left, right, name)
    501             if hasattr(lvalues, 'values'):
    502                 lvalues = lvalues.values
--> 503             return left._constructor(wrap_results(na_op(lvalues, rvalues)),
    504                                      index=left.index, name=left.name,
    505                                      dtype=dtype)

/usr/local/Cellar/python/2.7.6/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site-packages/pandas-0.13.1_780_gdc098d8-py2.7-macosx-10.9-x86_64.egg/pandas/core/ops.pyc in na_op(x, y)
    441         try:
    442             result = expressions.evaluate(op, str_rep, x, y,
--> 443                                           raise_on_error=True, **eval_kwargs)
    444         except TypeError:
    445             if isinstance(y, (pa.Array, pd.Series)):

/usr/local/Cellar/python/2.7.6/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site-packages/pandas-0.13.1_780_gdc098d8-py2.7-macosx-10.9-x86_64.egg/pandas/computation/expressions.pyc in evaluate(op, op_str, a, b, raise_on_error, use_numexpr, **eval_kwargs)
    185         use_numexpr : whether to try to use numexpr (default True)
    186         """
--> 187     _bool_arith_check(op_str, a, b)
    188     if use_numexpr:
    189         return _evaluate(op, op_str, a, b, raise_on_error=raise_on_error,

/usr/local/Cellar/python/2.7.6/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site-packages/pandas-0.13.1_780_gdc098d8-py2.7-macosx-10.9-x86_64.egg/pandas/computation/expressions.pyc in _bool_arith_check(op_str, a, b, not_allowed)
    164 def _bool_arith_check(op_str, a, b, not_allowed=frozenset(('+', '*', '-', '/',
    165                                                            '//', '**'))):
--> 166     if op_str in not_allowed and _has_bool_dtype(a) and _has_bool_dtype(b):
    167         raise NotImplementedError("operator %r not implemented for bool "
    168                                   "dtypes" % op_str)

/usr/local/Cellar/python/2.7.6/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site-packages/pandas-0.13.1_780_gdc098d8-py2.7-macosx-10.9-x86_64.egg/pandas/computation/expressions.pyc in _has_bool_dtype(x)
    159         return x.dtype == bool
    160     except AttributeError:
--> 161         return 'bool' in x.blocks
    162 
    163 

AttributeError: 'bool' object has no attribute 'blocks'
```

Running current master on Python 2.7.5 (OS X 10.9.2).



Discussion:
should be a better error message, but just do this

```
In [6]: s = Series([True,False,True])

In [7]: s ^ True
Out[7]: 
0    False
1     True
2    False
dtype: bool

In [8]: s & True
Out[8]: 
0     True
1    False
2     True
dtype: bool

In [12]: ~s
Out[12]: 
0    False
1     True
2    False
dtype: bool

```

@cpcloud looks like trivial fix in `pandas/computation/expressions/_has_bool_dtype`

pr on the way


Resolving patch:
diff --git a/pandas/computation/expressions.py b/pandas/computation/expressions.py
--- a/pandas/computation/expressions.py
+++ b/pandas/computation/expressions.py
@@ -158,7 +158,10 @@ def _has_bool_dtype(x):
     try:
         return x.dtype == bool
     except AttributeError:
-        return 'bool' in x.blocks
+        try:
+            return 'bool' in x.blocks
+        except AttributeError:
+            return isinstance(x, (bool, np.bool_))
 
 
 def _bool_arith_check(op_str, a, b, not_allowed=frozenset(('+', '*', '-', '/',
diff --git a/pandas/core/ops.py b/pandas/core/ops.py
--- a/pandas/core/ops.py
+++ b/pandas/core/ops.py
@@ -61,25 +61,25 @@ def names(x):
                               default_axis=default_axis, fill_zeros=np.inf),
         # Causes a floating point exception in the tests when numexpr
         # enabled, so for now no speedup
-        mod=arith_method(operator.mod, names('mod'), default_axis=default_axis,
-                         fill_zeros=np.nan),
+        mod=arith_method(operator.mod, names('mod'), None,
+                         default_axis=default_axis, fill_zeros=np.nan),
         pow=arith_method(operator.pow, names('pow'), op('**'),
                          default_axis=default_axis),
         # not entirely sure why this is necessary, but previously was included
         # so it's here to maintain compatibility
-        rmul=arith_method(operator.mul, names('rmul'),
+        rmul=arith_method(operator.mul, names('rmul'), op('*'),
                           default_axis=default_axis),
-        rsub=arith_method(lambda x, y: y - x, names('rsub'),
+        rsub=arith_method(lambda x, y: y - x, names('rsub'), op('-'),
                           default_axis=default_axis),
         rtruediv=arith_method(lambda x, y: operator.truediv(y, x),
-                              names('rtruediv'), truediv=True,
+                              names('rtruediv'), op('/'), truediv=True,
                               fill_zeros=np.inf, default_axis=default_axis),
         rfloordiv=arith_method(lambda x, y: operator.floordiv(y, x),
-                               names('rfloordiv'), default_axis=default_axis,
-                               fill_zeros=np.inf),
-        rpow=arith_method(lambda x, y: y ** x, names('rpow'),
+                               names('rfloordiv'), op('//'),
+                               default_axis=default_axis, fill_zeros=np.inf),
+        rpow=arith_method(lambda x, y: y ** x, names('rpow'), op('**'),
                           default_axis=default_axis),
-        rmod=arith_method(lambda x, y: y % x, names('rmod'),
+        rmod=arith_method(lambda x, y: y % x, names('rmod'), op('%'),
                           default_axis=default_axis),
     )
     new_methods['div'] = new_methods['truediv']
@@ -100,11 +100,11 @@ def names(x):
             and_=bool_method(operator.and_, names('and_'), op('&')),
             or_=bool_method(operator.or_, names('or_'), op('|')),
             # For some reason ``^`` wasn't used in original.
-            xor=bool_method(operator.xor, names('xor')),
+            xor=bool_method(operator.xor, names('xor'), op('^')),
             rand_=bool_method(lambda x, y: operator.and_(y, x),
-                              names('rand_')),
-            ror_=bool_method(lambda x, y: operator.or_(y, x), names('ror_')),
-            rxor=bool_method(lambda x, y: operator.xor(y, x), names('rxor'))
+                              names('rand_'), op('&')),
+            ror_=bool_method(lambda x, y: operator.or_(y, x), names('ror_'), op('|')),
+            rxor=bool_method(lambda x, y: operator.xor(y, x), names('rxor'), op('^'))
         ))
 
     new_methods = dict((names(k), v) for k, v in new_methods.items())
@@ -431,7 +431,7 @@ def maybe_convert_for_time_op(cls, left, right, name):
         return cls(left, right, name)
 
 
-def _arith_method_SERIES(op, name, str_rep=None, fill_zeros=None,
+def _arith_method_SERIES(op, name, str_rep, fill_zeros=None,
                          default_axis=None, **eval_kwargs):
     """
     Wrapper function for Series arithmetic operations, to avoid
@@ -506,7 +506,7 @@ def wrapper(left, right, name=name):
     return wrapper
 
 
-def _comp_method_SERIES(op, name, str_rep=None, masker=False):
+def _comp_method_SERIES(op, name, str_rep, masker=False):
     """
     Wrapper function for Series arithmetic operations, to avoid
     code duplication.
@@ -578,7 +578,7 @@ def wrapper(self, other):
     return wrapper
 
 
-def _bool_method_SERIES(op, name, str_rep=None):
+def _bool_method_SERIES(op, name, str_rep):
     """
     Wrapper function for Series arithmetic operations, to avoid
     code duplication.
@@ -647,7 +647,7 @@ def _radd_compat(left, right):
     return output
 
 
-def _flex_method_SERIES(op, name, str_rep=None, default_axis=None,
+def _flex_method_SERIES(op, name, str_rep, default_axis=None,
                         fill_zeros=None, **eval_kwargs):
     doc = """
     Binary operator %s with support to substitute a fill_value for missing data


Test patch:
diff --git a/pandas/tests/test_expressions.py b/pandas/tests/test_expressions.py
--- a/pandas/tests/test_expressions.py
+++ b/pandas/tests/test_expressions.py
@@ -357,6 +357,18 @@ def test_bool_ops_raise_on_arithmetic(self):
                 with tm.assertRaisesRegexp(NotImplementedError, err_msg):
                     f(df.a, df.b)
 
+                with tm.assertRaisesRegexp(NotImplementedError, err_msg):
+                    f(df.a, True)
+
+                with tm.assertRaisesRegexp(NotImplementedError, err_msg):
+                    f(False, df.a)
+
+                with tm.assertRaisesRegexp(TypeError, err_msg):
+                    f(False, df)
+
+                with tm.assertRaisesRegexp(TypeError, err_msg):
+                    f(df, True)
+
 
 if __name__ == '__main__':
     import nose