Repository: pandas-dev/pandas

Issue:
pandas.DataFrame.to_clipboard with excel option does not parse into columns with 0.23
#### Problem description

With Version 0.23 of pandas, the function .to_clipboard(), with the option excel=True no longer produces a copy of the dataframe which is Excel friendly when pasting the data into an Excel worksheet.  All the data comes into a single column in the Excel workbook.  Previously, the data would parse across all the columns.

The code that demonstrates this situation is:

```
Import pandas as pd
df = pd.DataFrame(np.random.randn(5,10))

df.to_clipboard(excel=True)
```

The output in excel ends up looking like this:

[https://www.screencast.com/t/DPTOukwE](url)

#### Output of ``pd.show_versions()``

<details>

INSTALLED VERSIONS
------------------
commit: None
python: 3.6.5.final.0
python-bits: 64
OS: Darwin
OS-release: 17.5.0
machine: x86_64
processor: i386
byteorder: little
LC_ALL: en_US.UTF-8
LANG: en_US.UTF-8
LOCALE: en_US.UTF-8

pandas: 0.23.0
pytest: 3.5.1
pip: 10.0.1
setuptools: 39.1.0
Cython: 0.28.2
numpy: 1.13.3
scipy: 1.1.0
pyarrow: None
xarray: None
IPython: 6.4.0
sphinx: 1.7.4
patsy: 0.5.0
dateutil: 2.7.3
pytz: 2018.4
blosc: None
bottleneck: 1.2.1
tables: 3.4.3
numexpr: 2.6.5
feather: None
matplotlib: 2.2.2
openpyxl: 2.5.3
xlrd: 1.1.0
xlwt: 1.2.0
xlsxwriter: 1.0.4
lxml: 4.2.1
bs4: 4.6.0
html5lib: 1.0.1
sqlalchemy: 1.2.7
pymysql: None
psycopg2: None
jinja2: 2.10
s3fs: None
fastparquet: None
pandas_gbq: None
pandas_datareader: None
</details>



Discussion:
Quick debug shows me that what was previously tab delimited is now showing up with spaces, hence why the paste to Excel does not work.

Investigation / PRs are welcome!
Ugh, sorry about this, I use this all the time and should have caught it in the RC.  I'll eventually get this, or please feel to take look first!

Resolving patch:
diff --git a/pandas/io/clipboards.py b/pandas/io/clipboards.py
--- a/pandas/io/clipboards.py
+++ b/pandas/io/clipboards.py
@@ -1,6 +1,7 @@
 """ io on the clipboard """
 from pandas import compat, get_option, option_context, DataFrame
-from pandas.compat import StringIO, PY2
+from pandas.compat import StringIO, PY2, PY3
+import warnings
 
 
 def read_clipboard(sep=r'\s+', **kwargs):  # pragma: no cover
@@ -32,7 +33,7 @@ def read_clipboard(sep=r'\s+', **kwargs):  # pragma: no cover
 
     # try to decode (if needed on PY3)
     # Strange. linux py33 doesn't complain, win py33 does
-    if compat.PY3:
+    if PY3:
         try:
             text = compat.bytes_to_str(
                 text, encoding=(kwargs.get('encoding') or
@@ -55,11 +56,27 @@ def read_clipboard(sep=r'\s+', **kwargs):  # pragma: no cover
 
     counts = {x.lstrip().count('\t') for x in lines}
     if len(lines) > 1 and len(counts) == 1 and counts.pop() != 0:
-        sep = r'\t'
+        sep = '\t'
 
+    # Edge case where sep is specified to be None, return to default
     if sep is None and kwargs.get('delim_whitespace') is None:
         sep = r'\s+'
 
+    # Regex separator currently only works with python engine.
+    # Default to python if separator is multi-character (regex)
+    if len(sep) > 1 and kwargs.get('engine') is None:
+        kwargs['engine'] = 'python'
+    elif len(sep) > 1 and kwargs.get('engine') == 'c':
+        warnings.warn('read_clipboard with regex separator does not work'
+                      ' properly with c engine')
+
+    # In PY2, the c table reader first encodes text with UTF-8 but Python
+    # table reader uses the format of the passed string. For consistency,
+    # encode strings for python engine so that output from python and c
+    # engines produce consistent results
+    if kwargs.get('engine') == 'python' and PY2:
+        text = text.encode('utf-8')
+
     return read_table(StringIO(text), sep=sep, **kwargs)
 
 
@@ -99,7 +116,7 @@ def to_clipboard(obj, excel=True, sep=None, **kwargs):  # pragma: no cover
     if excel:
         try:
             if sep is None:
-                sep = r'\t'
+                sep = '\t'
             buf = StringIO()
             # clipboard_set (pyperclip) expects unicode
             obj.to_csv(buf, sep=sep, encoding='utf-8', **kwargs)
@@ -108,8 +125,11 @@ def to_clipboard(obj, excel=True, sep=None, **kwargs):  # pragma: no cover
                 text = text.decode('utf-8')
             clipboard_set(text)
             return
-        except:
-            pass
+        except TypeError:
+            warnings.warn('to_clipboard in excel mode requires a single '
+                          'character separator.')
+    elif sep is not None:
+        warnings.warn('to_clipboard with excel=False ignores the sep argument')
 
     if isinstance(obj, DataFrame):
         # str(df) has various unhelpful defaults, like truncation


Test patch:
diff --git a/pandas/tests/io/test_clipboard.py b/pandas/tests/io/test_clipboard.py
--- a/pandas/tests/io/test_clipboard.py
+++ b/pandas/tests/io/test_clipboard.py
@@ -88,8 +88,6 @@ def check_round_trip_frame(self, data, excel=None, sep=None,
         tm.assert_frame_equal(data, result, check_dtype=False)
 
     # Test that default arguments copy as tab delimited
-    @pytest.mark.xfail(reason='to_clipboard defaults to space delim. '
-                       'Issue in #21104, Fixed in #21111')
     def test_round_trip_frame(self, df):
         self.check_round_trip_frame(df)
 
@@ -99,10 +97,6 @@ def test_round_trip_frame_sep(self, df, sep):
         self.check_round_trip_frame(df, sep=sep)
 
     # Test white space separator
-    @pytest.mark.xfail(reason="Fails on 'delims' df because quote escapes "
-                       "aren't handled correctly in default c engine. Fixed "
-                       "in #21111 by defaulting to python engine for "
-                       "whitespace separator")
     def test_round_trip_frame_string(self, df):
         df.to_clipboard(excel=False, sep=None)
         result = read_clipboard()
@@ -111,21 +105,17 @@ def test_round_trip_frame_string(self, df):
 
     # Two character separator is not supported in to_clipboard
     # Test that multi-character separators are not silently passed
-    @pytest.mark.xfail(reason="Not yet implemented.  Fixed in #21111")
     def test_excel_sep_warning(self, df):
         with tm.assert_produces_warning():
             df.to_clipboard(excel=True, sep=r'\t')
 
     # Separator is ignored when excel=False and should produce a warning
-    @pytest.mark.xfail(reason="Not yet implemented.  Fixed in #21111")
     def test_copy_delim_warning(self, df):
         with tm.assert_produces_warning():
             df.to_clipboard(excel=False, sep='\t')
 
     # Tests that the default behavior of to_clipboard is tab
     # delimited and excel="True"
-    @pytest.mark.xfail(reason="to_clipboard defaults to space delim. Issue in "
-                       "#21104, Fixed in #21111")
     @pytest.mark.parametrize('sep', ['\t', None, 'default'])
     @pytest.mark.parametrize('excel', [True, None, 'default'])
     def test_clipboard_copy_tabs_default(self, sep, excel, df):
@@ -139,10 +129,6 @@ def test_clipboard_copy_tabs_default(self, sep, excel, df):
             assert clipboard_get() == df.to_csv(sep='\t')
 
     # Tests reading of white space separated tables
-    @pytest.mark.xfail(reason="Fails on 'delims' df because quote escapes "
-                       "aren't handled correctly. in default c engine. Fixed "
-                       "in #21111 by defaulting to python engine for "
-                       "whitespace separator")
     @pytest.mark.parametrize('sep', [None, 'default'])
     @pytest.mark.parametrize('excel', [False])
     def test_clipboard_copy_strings(self, sep, excel, df):
@@ -193,8 +179,6 @@ def test_invalid_encoding(self, df):
         with pytest.raises(NotImplementedError):
             pd.read_clipboard(encoding='ascii')
 
-    @pytest.mark.xfail(reason='to_clipboard defaults to space delim. '
-                       'Issue in #21104, Fixed in #21111')
     @pytest.mark.parametrize('enc', ['UTF-8', 'utf-8', 'utf8'])
     def test_round_trip_valid_encodings(self, enc, df):
         self.check_round_trip_frame(df, encoding=enc)