Repository: pandas-dev/pandas
Pull request: check for datetime+period addition
- [x] closes #17983
- [x] tests added / passed
- [x] passes `git diff upstream/master -u -- "*.py" | flake8 --diff`
- [x] whatsnew entry


Problem statement:
Series[Period] and Timestamp binop causes segfault
No time to look atm:

```
In [1]: import pandas as pd

In [2]: pd.Timestamp('2017') + pd.Series([pd.Period('2017', freq='M')])
Fatal Python error: Cannot recover from stack overflow.

Thread 0x00007000053d1000 (most recent call first):
  File "/usr/local/Cellar/python3/3.6.1/Frameworks/Python.framework/Versions/3.6/lib/python3.6/threading.py", line 295 in wait
  File "/usr/local/Cellar/python3/3.6.1/Frameworks/Python.framework/Versions/3.6/lib/python3.6/threading.py", line 551 in wait
  File "/Users/taugspurger/Envs/pandas-dev/lib/python3.6/site-packages/IPython/core/history.py", line 829 in run
  File "/Users/taugspurger/Envs/pandas-dev/lib/python3.6/site-packages/IPython/core/history.py", line 58 in needs_sqlite
  File "<decorator-gen-24>", line 2 in run
  File "/usr/local/Cellar/python3/3.6.1/Frameworks/Python.framework/Versions/3.6/lib/python3.6/threading.py", line 916 in _bootstrap_inner
  File "/usr/local/Cellar/python3/3.6.1/Frameworks/Python.framework/Versions/3.6/lib/python3.6/threading.py", line 884 in _bootstrap

Current thread 0x00007fff943f83c0 (most recent call first):
  File "/Users/taugspurger/Envs/pandas-dev/lib/python3.6/site-packages/pandas/pandas/core/ops.py", line 78 in <lambda>
  File "/Users/taugspurger/Envs/pandas-dev/lib/python3.6/site-packages/pandas/pandas/core/ops.py", line 713 in <lambda>
  File "/Users/taugspurger/Envs/pandas-dev/lib/python3.6/site-packages/pandas/pandas/core/ops.py", line 713 in safe_na_op
  File "/Users/taugspurger/Envs/pandas-dev/lib/python3.6/site-packages/pandas/pandas/core/ops.py", line 742 in wrapper
  File "<ipython-input-2-5cbafe935254>", line 1 in <module>
  File "/Users/taugspurger/Envs/pandas-dev/lib/python3.6/site-packages/IPython/core/interactiveshell.py", line 2862 in run_code
  File "/Users/taugspurger/Envs/pandas-dev/lib/python3.6/site-packages/IPython/core/interactiveshell.py", line 2808 in run_ast_nodes
  File "/Users/taugspurger/Envs/pandas-dev/lib/python3.6/site-packages/IPython/core/interactiveshell.py", line 2698 in run_cell
  File "/Users/taugspurger/Envs/pandas-dev/lib/python3.6/site-packages/IPython/terminal/interactiveshell.py", line 471 in interact
  File "/Users/taugspurger/Envs/pandas-dev/lib/python3.6/site-packages/IPython/terminal/interactiveshell.py", line 480 in mainloop
  File "/Users/taugspurger/Envs/pandas-dev/lib/python3.6/site-packages/IPython/terminal/ipapp.py", line 356 in start
  File "/Users/taugspurger/Envs/pandas-dev/lib/python3.6/site-packages/traitlets/config/application.py", line 658 in launch_instance
  File "/Users/taugspurger/Envs/pandas-dev/lib/python3.6/site-packages/IPython/__init__.py", line 125 in start_ipython
  File "/Users/taugspurger/.virtualenvs/pandas-dev/bin/ipython", line 11 in <module>
```

The other way hits a maximum recursion error:

```python
In [1]: import pandas as pd

In [2]: pd.Series([pd.Period('2017', freq='M')]) + pd.Timestamp('2017')


---------------------------------------------------------------------------
RecursionError                            Traceback (most recent call last)
~/Envs/pandas-dev/lib/python3.6/site-packages/pandas/pandas/core/ops.py in safe_na_op(lvalues, rvalues)
    698             with np.errstate(all='ignore'):
--> 699                 return na_op(lvalues, rvalues)
    700         except Exception:

~/Envs/pandas-dev/lib/python3.6/site-packages/pandas/pandas/core/ops.py in na_op(x, y)
    674         try:
--> 675             result = expressions.evaluate(op, str_rep, x, y, **eval_kwargs)
    676         except TypeError:

~/Envs/pandas-dev/lib/python3.6/site-packages/pandas/pandas/core/computation/expressions.py in evaluate(op, op_str, a, b, use_numexpr, **eval_kwargs)
    203     if use_numexpr:
--> 204         return _evaluate(op, op_str, a, b, **eval_kwargs)
    205     return _evaluate_standard(op, op_str, a, b)

~/Envs/pandas-dev/lib/python3.6/site-packages/pandas/pandas/core/computation/expressions.py in _evaluate_numexpr(op, op_str, a, b, truediv, reversed, **eval_kwargs)
    118     if result is None:
--> 119         result = _evaluate_standard(op, op_str, a, b)
    120

~/Envs/pandas-dev/lib/python3.6/site-packages/pandas/pandas/core/computation/expressions.py in _evaluate_standard(op, op_str, a, b, **eval_kwargs)
     63     with np.errstate(all='ignore'):
---> 64         return op(a, b)
     65

~/Envs/pandas-dev/lib/python3.6/site-packages/pandas/pandas/_libs/tslib.pyx in pandas._libs.tslib.Timestamp.__radd__()

~/Envs/pandas-dev/lib/python3.6/site-packages/pandas/pandas/_libs/period.pyx in pandas._libs.period._Period.__add__()

... last 2 frames repeated, from the frame below ...

~/Envs/pandas-dev/lib/python3.6/site-packages/pandas/pandas/_libs/tslib.pyx in pandas._libs.tslib.Timestamp.__radd__()

RecursionError: maximum recursion depth exceeded while calling a Python object

During handling of the above exception, another exception occurred:

RecursionError                            Traceback (most recent call last)
<ipython-input-2-18743c02baa6> in <module>()
----> 1 pd.Series([pd.Period('2017', freq='M')]) + pd.Timestamp('2017')

~/Envs/pandas-dev/lib/python3.6/site-packages/pandas/pandas/core/ops.py in wrapper(left, right, name, na_op)
    736                 lvalues = lvalues.values
    737
--> 738         result = wrap_results(safe_na_op(lvalues, rvalues))
    739         return construct_result(
    740             left,

~/Envs/pandas-dev/lib/python3.6/site-packages/pandas/pandas/core/ops.py in safe_na_op(lvalues, rvalues)
    707                 if is_object_dtype(lvalues):
    708                     return libalgos.arrmap_object(lvalues,
--> 709                                                   lambda x: op(x, rvalues))
    710             raise
    711

~/Envs/pandas-dev/lib/python3.6/site-packages/pandas/pandas/_libs/algos_common_helper.pxi in pandas._libs.algos.arrmap_object()

~/Envs/pandas-dev/lib/python3.6/site-packages/pandas/pandas/core/ops.py in <lambda>(x)
    707                 if is_object_dtype(lvalues):
    708                     return libalgos.arrmap_object(lvalues,
--> 709                                                   lambda x: op(x, rvalues))
    710             raise
    711

~/Envs/pandas-dev/lib/python3.6/site-packages/pandas/pandas/_libs/tslib.pyx in pandas._libs.tslib.Timestamp.__radd__()

~/Envs/pandas-dev/lib/python3.6/site-packages/pandas/pandas/_libs/period.pyx in pandas._libs.period._Period.__add__()

... last 2 frames repeated, from the frame below ...

~/Envs/pandas-dev/lib/python3.6/site-packages/pandas/pandas/_libs/tslib.pyx in pandas._libs.tslib.Timestamp.__radd__()

RecursionError: maximum recursion depth exceeded while calling a Python object

```

Hints from issue discussion:
FWIW the segfault appears to be specific to IPython.  In the standard interpreter I just get the `RecursionError` in both py2+py3.  Should be an easy patch; should I hold off until after the next release?
No rush, I don't think this is a major issue. I think we're doing the final release tomorrow or Friday.

Commit message: check for datetime+period addition, closed#17983

Patch to review:
diff --git a/pandas/_libs/period.pyx b/pandas/_libs/period.pyx
index 2b09e9376bd3d..b95632b5b0eff 100644
--- a/pandas/_libs/period.pyx
+++ b/pandas/_libs/period.pyx
@@ -17,6 +17,10 @@ from pandas.compat import PY2
 
 cimport cython
 
+from cpython.datetime cimport PyDateTime_Check, PyDateTime_IMPORT
+# import datetime C API
+PyDateTime_IMPORT
+
 from tslibs.np_datetime cimport (pandas_datetimestruct,
                                  dtstruct_to_dt64, dt64_to_dtstruct,
                                  is_leapyear)
@@ -647,9 +651,19 @@ cdef class _Period(object):
             elif util.is_integer_object(other):
                 ordinal = self.ordinal + other * self.freq.n
                 return Period(ordinal=ordinal, freq=self.freq)
+            elif (PyDateTime_Check(other) or
+                  is_period_object(other) or util.is_datetime64_object(other)):
+                # can't add datetime-like
+                # GH#17983
+                sname = type(self).__name__
+                oname = type(other).__name__
+                raise TypeError("unsupported operand type(s) for +: '{self}' "
+                                "and '{other}'".format(self=sname,
+                                                       other=oname))
             else:  # pragma: no cover
                 return NotImplemented
         elif is_period_object(other):
+            # this can be reached via __radd__ because of cython rules
             return other + self
         else:
             return NotImplemented
diff --git a/pandas/tests/scalar/test_period.py b/pandas/tests/scalar/test_period.py
index 8cfdf7a461879..da36729cb734b 100644
--- a/pandas/tests/scalar/test_period.py
+++ b/pandas/tests/scalar/test_period.py
@@ -1038,6 +1038,24 @@ def test_add_raises(self):
         with tm.assert_raises_regex(TypeError, msg):
             dt1 + dt2
 
+    boxes = [lambda x: x, lambda x: pd.Series([x]), lambda x: pd.Index([x])]
+
+    @pytest.mark.parametrize('lbox', boxes)
+    @pytest.mark.parametrize('rbox', boxes)
+    def test_add_timestamp_raises(self, rbox, lbox):
+        # GH # 17983
+        ts = pd.Timestamp('2017')
+        per = pd.Period('2017', freq='M')
+
+        with pytest.raises(TypeError):
+            lbox(ts) + rbox(per)
+
+        with pytest.raises(TypeError):
+            lbox(per) + rbox(ts)
+
+        with pytest.raises(TypeError):
+            lbox(per) + rbox(per)
+
     def test_sub(self):
         dt1 = Period('2011-01-01', freq='D')
         dt2 = Period('2011-01-15', freq='D')


Code review comments:
diff_hunk: @@ -1038,6 +1038,24 @@ def test_add_raises(self):
         with tm.assert_raises_regex(TypeError, msg):
             dt1 + dt2
 
+    boxes = [lambda x: x, lambda x: pd.Series([x]), lambda x: pd.Index([x])]
+
+    @pytest.mark.parametrize('lbox', boxes)
+    @pytest.mark.parametrize('rbox', boxes)
+    def test_add_timestamp_raises(self, rbox, lbox):
+        # GH # 17983
+        ts = pd.Timestamp('2017')
+        per = pd.Period('2017', freq='M')
+
+        with pytest.raises(TypeError):
path: pandas/tests/scalar/test_period.py
text: @user1:
Let's make sure that the right error message is hit (use `tm.assert_raises_regex` to be safe, unclear ATM whether we're going to leverage the most recent `pytest` API).