From 33cd2d72e0c007c460e59105efda9211441b2ce4 Mon Sep 17 00:00:00 2001 From: krey Date: Mon, 16 Sep 2019 12:38:21 -0400 Subject: [PATCH 1/8] Updated internal pyperclip 1.5.27 -> 1.7.0 --- doc/source/whatsnew/v1.0.0.rst | 1 + pandas/io/clipboard/__init__.py | 652 +++++++++++++++++++++++++++--- pandas/io/clipboard/clipboards.py | 129 ------ pandas/io/clipboard/exceptions.py | 11 - pandas/io/clipboard/windows.py | 184 --------- 5 files changed, 592 insertions(+), 385 deletions(-) delete mode 100644 pandas/io/clipboard/clipboards.py delete mode 100644 pandas/io/clipboard/exceptions.py delete mode 100644 pandas/io/clipboard/windows.py diff --git a/doc/source/whatsnew/v1.0.0.rst b/doc/source/whatsnew/v1.0.0.rst index b40a64420a0be..370e1c09d33aa 100644 --- a/doc/source/whatsnew/v1.0.0.rst +++ b/doc/source/whatsnew/v1.0.0.rst @@ -396,6 +396,7 @@ I/O - Bug in :meth:`pandas.io.formats.style.Styler` formatting for floating values not displaying decimals correctly (:issue:`13257`) - Bug in :meth:`DataFrame.to_html` when using ``formatters=`` and ``max_cols`` together. (:issue:`25955`) - Bug in :meth:`Styler.background_gradient` not able to work with dtype ``Int64`` (:issue:`28869`) +- Bug in :meth:`DataFrame.to_clipboard` which did not work reliably in ipython (:issue:`22707`) Plotting ^^^^^^^^ diff --git a/pandas/io/clipboard/__init__.py b/pandas/io/clipboard/__init__.py index caa928731fb3a..ecacba3a93ae7 100644 --- a/pandas/io/clipboard/__init__.py +++ b/pandas/io/clipboard/__init__.py @@ -1,7 +1,7 @@ """ Pyperclip -A cross-platform clipboard module for Python. (only handles plain text for now) +A cross-platform clipboard module for Python, with copy & paste functions for plain text. By Al Sweigart al@inventwithpython.com BSD License @@ -10,117 +10,647 @@ pyperclip.copy('The text to be copied to the clipboard.') spam = pyperclip.paste() - if not pyperclip.copy: + if not pyperclip.is_available(): print("Copy functionality unavailable!") On Windows, no additional modules are needed. -On Mac, the module uses pbcopy and pbpaste, which should come with the os. +On Mac, the pyobjc module is used, falling back to the pbcopy and pbpaste cli + commands. (These commands should come with OS X.). On Linux, install xclip or xsel via package manager. For example, in Debian: -sudo apt-get install xclip + sudo apt-get install xclip + sudo apt-get install xsel -Otherwise on Linux, you will need the qtpy or PyQt modules installed. -qtpy also requires a python-qt-bindings module: PyQt4, PyQt5, PySide, PySide2 +Otherwise on Linux, you will need the gtk or PyQt5/PyQt4 modules installed. + +gtk and PyQt4 modules are not available for Python 3, +and this module does not work with PyGObject yet. + +Note: There seems to be a way to get gtk on Python 3, according to: + https://askubuntu.com/questions/697397/python3-is-not-supporting-gtk-module + +Cygwin is currently not supported. + +Security Note: This module runs programs with these names: + - which + - where + - pbcopy + - pbpaste + - xclip + - xsel + - klipper + - qdbus +A malicious user could rename or add programs with these names, tricking +Pyperclip into running them with whatever permissions the Python process has. -This module does not work with PyGObject yet. """ -__version__ = "1.5.27" +__version__ = '1.7.0' +import contextlib +import ctypes import os import platform import subprocess +import sys +import time +import warnings + +from ctypes import c_size_t, sizeof, c_wchar_p, get_errno, c_wchar -from .clipboards import ( - init_klipper_clipboard, - init_no_clipboard, - init_osx_clipboard, - init_qt_clipboard, - init_xclip_clipboard, - init_xsel_clipboard, -) -from .windows import init_windows_clipboard - -# `import qtpy` sys.exit()s if DISPLAY is not in the environment. + +# `import PyQt4` sys.exit()s if DISPLAY is not in the environment. # Thus, we need to detect the presence of $DISPLAY manually -# and not load qtpy if it is absent. +# and not load PyQt4 if it is absent. HAS_DISPLAY = os.getenv("DISPLAY", False) -CHECK_CMD = "where" if platform.system() == "Windows" else "which" +EXCEPT_MSG = """ + Pyperclip could not find a copy/paste mechanism for your system. + For more information, please visit https://pyperclip.readthedocs.io/en/latest/introduction.html#not-implemented-error """ + +PY2 = sys.version_info[0] == 2 + +STR_OR_UNICODE = unicode if PY2 else str # For paste(): Python 3 uses str, Python 2 uses unicode. + +ENCODING = 'utf-8' + +# The "which" unix command finds where a command is. +if platform.system() == 'Windows': + WHICH_CMD = 'where' +else: + WHICH_CMD = 'which' def _executable_exists(name): - return ( - subprocess.call( - [CHECK_CMD, name], stdout=subprocess.PIPE, stderr=subprocess.PIPE - ) - == 0 - ) + return subprocess.call([WHICH_CMD, name], + stdout=subprocess.PIPE, stderr=subprocess.PIPE) == 0 + + + +# Exceptions +class PyperclipException(RuntimeError): + pass + +class PyperclipWindowsException(PyperclipException): + def __init__(self, message): + message += " (%s)" % ctypes.WinError() + super(PyperclipWindowsException, self).__init__(message) + + +def _stringifyText(text): + if PY2: + acceptedTypes = (unicode, str, int, float, bool) + else: + acceptedTypes = (str, int, float, bool) + if not isinstance(text, acceptedTypes): + raise PyperclipException('only str, int, float, and bool values can be copied to the clipboard, not %s' % (text.__class__.__name__)) + return STR_OR_UNICODE(text) + + +def init_osx_pbcopy_clipboard(): + + def copy_osx_pbcopy(text): + text = _stringifyText(text) # Converts non-str values to str. + p = subprocess.Popen(['pbcopy', 'w'], + stdin=subprocess.PIPE, close_fds=True) + p.communicate(input=text.encode(ENCODING)) + + def paste_osx_pbcopy(): + p = subprocess.Popen(['pbpaste', 'r'], + stdout=subprocess.PIPE, close_fds=True) + stdout, stderr = p.communicate() + return stdout.decode(ENCODING) + + return copy_osx_pbcopy, paste_osx_pbcopy + + +def init_osx_pyobjc_clipboard(): + def copy_osx_pyobjc(text): + '''Copy string argument to clipboard''' + text = _stringifyText(text) # Converts non-str values to str. + newStr = Foundation.NSString.stringWithString_(text).nsstring() + newData = newStr.dataUsingEncoding_(Foundation.NSUTF8StringEncoding) + board = AppKit.NSPasteboard.generalPasteboard() + board.declareTypes_owner_([AppKit.NSStringPboardType], None) + board.setData_forType_(newData, AppKit.NSStringPboardType) + + def paste_osx_pyobjc(): + "Returns contents of clipboard" + board = AppKit.NSPasteboard.generalPasteboard() + content = board.stringForType_(AppKit.NSStringPboardType) + return content + + return copy_osx_pyobjc, paste_osx_pyobjc + + +def init_gtk_clipboard(): + global gtk + import gtk + + def copy_gtk(text): + global cb + text = _stringifyText(text) # Converts non-str values to str. + cb = gtk.Clipboard() + cb.set_text(text) + cb.store() + + def paste_gtk(): + clipboardContents = gtk.Clipboard().wait_for_text() + # for python 2, returns None if the clipboard is blank. + if clipboardContents is None: + return '' + else: + return clipboardContents + + return copy_gtk, paste_gtk + + +def init_qt_clipboard(): + global QApplication + # $DISPLAY should exist + + # Try to import from qtpy, but if that fails try PyQt5 then PyQt4 + try: + from qtpy.QtWidgets import QApplication + except: + try: + from PyQt5.QtWidgets import QApplication + except: + from PyQt4.QtGui import QApplication + + app = QApplication.instance() + if app is None: + app = QApplication([]) + + def copy_qt(text): + text = _stringifyText(text) # Converts non-str values to str. + cb = app.clipboard() + cb.setText(text) + + def paste_qt(): + cb = app.clipboard() + return STR_OR_UNICODE(cb.text()) + + return copy_qt, paste_qt + + +def init_xclip_clipboard(): + DEFAULT_SELECTION='c' + PRIMARY_SELECTION='p' + + def copy_xclip(text, primary=False): + text = _stringifyText(text) # Converts non-str values to str. + selection=DEFAULT_SELECTION + if primary: + selection=PRIMARY_SELECTION + p = subprocess.Popen(['xclip', '-selection', selection], + stdin=subprocess.PIPE, close_fds=True) + p.communicate(input=text.encode(ENCODING)) + + def paste_xclip(primary=False): + selection=DEFAULT_SELECTION + if primary: + selection=PRIMARY_SELECTION + p = subprocess.Popen(['xclip', '-selection', selection, '-o'], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + close_fds=True) + stdout, stderr = p.communicate() + # Intentionally ignore extraneous output on stderr when clipboard is empty + return stdout.decode(ENCODING) + + return copy_xclip, paste_xclip + + +def init_xsel_clipboard(): + DEFAULT_SELECTION='-b' + PRIMARY_SELECTION='-p' + + def copy_xsel(text, primary=False): + text = _stringifyText(text) # Converts non-str values to str. + selection_flag = DEFAULT_SELECTION + if primary: + selection_flag = PRIMARY_SELECTION + p = subprocess.Popen(['xsel', selection_flag, '-i'], + stdin=subprocess.PIPE, close_fds=True) + p.communicate(input=text.encode(ENCODING)) + + def paste_xsel(primary=False): + selection_flag = DEFAULT_SELECTION + if primary: + selection_flag = PRIMARY_SELECTION + p = subprocess.Popen(['xsel', selection_flag, '-o'], + stdout=subprocess.PIPE, close_fds=True) + stdout, stderr = p.communicate() + return stdout.decode(ENCODING) + + return copy_xsel, paste_xsel + + +def init_klipper_clipboard(): + def copy_klipper(text): + text = _stringifyText(text) # Converts non-str values to str. + p = subprocess.Popen( + ['qdbus', 'org.kde.klipper', '/klipper', 'setClipboardContents', + text.encode(ENCODING)], + stdin=subprocess.PIPE, close_fds=True) + p.communicate(input=None) + + def paste_klipper(): + p = subprocess.Popen( + ['qdbus', 'org.kde.klipper', '/klipper', 'getClipboardContents'], + stdout=subprocess.PIPE, close_fds=True) + stdout, stderr = p.communicate() + + # Workaround for https://bugs.kde.org/show_bug.cgi?id=342874 + # TODO: https://github.com/asweigart/pyperclip/issues/43 + clipboardContents = stdout.decode(ENCODING) + # even if blank, Klipper will append a newline at the end + assert len(clipboardContents) > 0 + # make sure that newline is there + assert clipboardContents.endswith('\n') + if clipboardContents.endswith('\n'): + clipboardContents = clipboardContents[:-1] + return clipboardContents + + return copy_klipper, paste_klipper + + +def init_dev_clipboard_clipboard(): + def copy_dev_clipboard(text): + text = _stringifyText(text) # Converts non-str values to str. + if text == '': + warnings.warn('Pyperclip cannot copy a blank string to the clipboard on Cygwin. This is effectively a no-op.') + if '\r' in text: + warnings.warn('Pyperclip cannot handle \\r characters on Cygwin.') + + fo = open('/dev/clipboard', 'wt') + fo.write(text) + fo.close() + + def paste_dev_clipboard(): + fo = open('/dev/clipboard', 'rt') + content = fo.read() + fo.close() + return content + + return copy_dev_clipboard, paste_dev_clipboard + + +def init_no_clipboard(): + class ClipboardUnavailable(object): + + def __call__(self, *args, **kwargs): + raise PyperclipException(EXCEPT_MSG) + + if PY2: + def __nonzero__(self): + return False + else: + def __bool__(self): + return False + + return ClipboardUnavailable(), ClipboardUnavailable() + + + + +# Windows-related clipboard functions: +class CheckedCall(object): + def __init__(self, f): + super(CheckedCall, self).__setattr__("f", f) + + def __call__(self, *args): + ret = self.f(*args) + if not ret and get_errno(): + raise PyperclipWindowsException("Error calling " + self.f.__name__) + return ret + + def __setattr__(self, key, value): + setattr(self.f, key, value) + +def init_windows_clipboard(): + global HGLOBAL, LPVOID, DWORD, LPCSTR, INT, HWND, HINSTANCE, HMENU, BOOL, UINT, HANDLE + from ctypes.wintypes import (HGLOBAL, LPVOID, DWORD, LPCSTR, INT, HWND, + HINSTANCE, HMENU, BOOL, UINT, HANDLE) + windll = ctypes.windll + msvcrt = ctypes.CDLL('msvcrt') + + safeCreateWindowExA = CheckedCall(windll.user32.CreateWindowExA) + safeCreateWindowExA.argtypes = [DWORD, LPCSTR, LPCSTR, DWORD, INT, INT, + INT, INT, HWND, HMENU, HINSTANCE, LPVOID] + safeCreateWindowExA.restype = HWND + + safeDestroyWindow = CheckedCall(windll.user32.DestroyWindow) + safeDestroyWindow.argtypes = [HWND] + safeDestroyWindow.restype = BOOL + + OpenClipboard = windll.user32.OpenClipboard + OpenClipboard.argtypes = [HWND] + OpenClipboard.restype = BOOL + + safeCloseClipboard = CheckedCall(windll.user32.CloseClipboard) + safeCloseClipboard.argtypes = [] + safeCloseClipboard.restype = BOOL + + safeEmptyClipboard = CheckedCall(windll.user32.EmptyClipboard) + safeEmptyClipboard.argtypes = [] + safeEmptyClipboard.restype = BOOL + + safeGetClipboardData = CheckedCall(windll.user32.GetClipboardData) + safeGetClipboardData.argtypes = [UINT] + safeGetClipboardData.restype = HANDLE + + safeSetClipboardData = CheckedCall(windll.user32.SetClipboardData) + safeSetClipboardData.argtypes = [UINT, HANDLE] + safeSetClipboardData.restype = HANDLE + + safeGlobalAlloc = CheckedCall(windll.kernel32.GlobalAlloc) + safeGlobalAlloc.argtypes = [UINT, c_size_t] + safeGlobalAlloc.restype = HGLOBAL + + safeGlobalLock = CheckedCall(windll.kernel32.GlobalLock) + safeGlobalLock.argtypes = [HGLOBAL] + safeGlobalLock.restype = LPVOID + + safeGlobalUnlock = CheckedCall(windll.kernel32.GlobalUnlock) + safeGlobalUnlock.argtypes = [HGLOBAL] + safeGlobalUnlock.restype = BOOL + + wcslen = CheckedCall(msvcrt.wcslen) + wcslen.argtypes = [c_wchar_p] + wcslen.restype = UINT + + GMEM_MOVEABLE = 0x0002 + CF_UNICODETEXT = 13 + + @contextlib.contextmanager + def window(): + """ + Context that provides a valid Windows hwnd. + """ + # we really just need the hwnd, so setting "STATIC" + # as predefined lpClass is just fine. + hwnd = safeCreateWindowExA(0, b"STATIC", None, 0, 0, 0, 0, 0, + None, None, None, None) + try: + yield hwnd + finally: + safeDestroyWindow(hwnd) + + @contextlib.contextmanager + def clipboard(hwnd): + """ + Context manager that opens the clipboard and prevents + other applications from modifying the clipboard content. + """ + # We may not get the clipboard handle immediately because + # some other application is accessing it (?) + # We try for at least 500ms to get the clipboard. + t = time.time() + 0.5 + success = False + while time.time() < t: + success = OpenClipboard(hwnd) + if success: + break + time.sleep(0.01) + if not success: + raise PyperclipWindowsException("Error calling OpenClipboard") + + try: + yield + finally: + safeCloseClipboard() + + def copy_windows(text): + # This function is heavily based on + # http://msdn.com/ms649016#_win32_Copying_Information_to_the_Clipboard + + text = _stringifyText(text) # Converts non-str values to str. + + with window() as hwnd: + # http://msdn.com/ms649048 + # If an application calls OpenClipboard with hwnd set to NULL, + # EmptyClipboard sets the clipboard owner to NULL; + # this causes SetClipboardData to fail. + # => We need a valid hwnd to copy something. + with clipboard(hwnd): + safeEmptyClipboard() + + if text: + # http://msdn.com/ms649051 + # If the hMem parameter identifies a memory object, + # the object must have been allocated using the + # function with the GMEM_MOVEABLE flag. + count = wcslen(text) + 1 + handle = safeGlobalAlloc(GMEM_MOVEABLE, + count * sizeof(c_wchar)) + locked_handle = safeGlobalLock(handle) + + ctypes.memmove(c_wchar_p(locked_handle), c_wchar_p(text), count * sizeof(c_wchar)) + + safeGlobalUnlock(handle) + safeSetClipboardData(CF_UNICODETEXT, handle) + + def paste_windows(): + with clipboard(None): + handle = safeGetClipboardData(CF_UNICODETEXT) + if not handle: + # GetClipboardData may return NULL with errno == NO_ERROR + # if the clipboard is empty. + # (Also, it may return a handle to an empty buffer, + # but technically that's not empty) + return "" + return c_wchar_p(handle).value + + return copy_windows, paste_windows + + +def init_wsl_clipboard(): + def copy_wsl(text): + text = _stringifyText(text) # Converts non-str values to str. + p = subprocess.Popen(['clip.exe'], + stdin=subprocess.PIPE, close_fds=True) + p.communicate(input=text.encode(ENCODING)) + + def paste_wsl(): + p = subprocess.Popen(['powershell.exe', '-command', 'Get-Clipboard'], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + close_fds=True) + stdout, stderr = p.communicate() + # WSL appends "\r\n" to the contents. + return stdout[:-2].decode(ENCODING) + + return copy_wsl, paste_wsl + + +# Automatic detection of clipboard mechanisms and importing is done in deteremine_clipboard(): def determine_clipboard(): - # Determine the OS/platform and set - # the copy() and paste() functions accordingly. - if "cygwin" in platform.system().lower(): + ''' + Determine the OS/platform and set the copy() and paste() functions + accordingly. + ''' + + global Foundation, AppKit, gtk, qtpy, PyQt4, PyQt5 + + # Setup for the CYGWIN platform: + if 'cygwin' in platform.system().lower(): # Cygwin has a variety of values returned by platform.system(), such as 'CYGWIN_NT-6.1' # FIXME: pyperclip currently does not support Cygwin, # see https://github.com/asweigart/pyperclip/issues/55 - pass - elif os.name == "nt" or platform.system() == "Windows": + if os.path.exists('/dev/clipboard'): + warnings.warn('Pyperclip\'s support for Cygwin is not perfect, see https://github.com/asweigart/pyperclip/issues/55') + return init_dev_clipboard_clipboard() + + # Setup for the WINDOWS platform: + elif os.name == 'nt' or platform.system() == 'Windows': return init_windows_clipboard() - if os.name == "mac" or platform.system() == "Darwin": - return init_osx_clipboard() + + if platform.system() == 'Linux': + with open('/proc/version', 'r') as f: + if "Microsoft" in f.read(): + return init_wsl_clipboard() + + # Setup for the MAC OS X platform: + if os.name == 'mac' or platform.system() == 'Darwin': + try: + import Foundation # check if pyobjc is installed + import AppKit + except ImportError: + return init_osx_pbcopy_clipboard() + else: + return init_osx_pyobjc_clipboard() + + # Setup for the LINUX platform: if HAS_DISPLAY: - # Determine which command/module is installed, if any. try: - # qtpy is a small abstraction layer that lets you write - # applications using a single api call to either PyQt or PySide - # https://pypi.org/project/QtPy - import qtpy # noqa + import gtk # check if gtk is installed + except ImportError: + pass # We want to fail fast for all non-ImportError exceptions. + else: + return init_gtk_clipboard() + + if _executable_exists("xsel"): + return init_xsel_clipboard() + if _executable_exists("xclip"): + return init_xclip_clipboard() + if _executable_exists("klipper") and _executable_exists("qdbus"): + return init_klipper_clipboard() + + try: + # qtpy is a small abstraction layer that lets you write applications using a single api call to either PyQt or PySide. + # https://pypi.python.org/pypi/QtPy + import qtpy # check if qtpy is installed except ImportError: - # If qtpy isn't installed, fall back on importing PyQt5, or PyQt5 + # If qtpy isn't installed, fall back on importing PyQt4. try: - import PyQt5 # noqa + import PyQt5 # check if PyQt5 is installed except ImportError: try: - import PyQt4 # noqa + import PyQt4 # check if PyQt4 is installed except ImportError: - pass # fail fast for all non-ImportError exceptions. + pass # We want to fail fast for all non-ImportError exceptions. else: return init_qt_clipboard() else: return init_qt_clipboard() - pass else: return init_qt_clipboard() - if _executable_exists("xclip"): - return init_xclip_clipboard() - if _executable_exists("xsel"): - return init_xsel_clipboard() - if _executable_exists("klipper") and _executable_exists("qdbus"): - return init_klipper_clipboard() return init_no_clipboard() def set_clipboard(clipboard): + ''' + Explicitly sets the clipboard mechanism. The "clipboard mechanism" is how + the copy() and paste() functions interact with the operating system to + implement the copy/paste feature. The clipboard parameter must be one of: + - pbcopy + - pbobjc (default on Mac OS X) + - gtk + - qt + - xclip + - xsel + - klipper + - windows (default on Windows) + - no (this is what is set when no clipboard mechanism can be found) + ''' global copy, paste - clipboard_types = { - "osx": init_osx_clipboard, - "qt": init_qt_clipboard, - "xclip": init_xclip_clipboard, - "xsel": init_xsel_clipboard, - "klipper": init_klipper_clipboard, - "windows": init_windows_clipboard, - "no": init_no_clipboard, - } + clipboard_types = {'pbcopy': init_osx_pbcopy_clipboard, + 'pyobjc': init_osx_pyobjc_clipboard, + 'gtk': init_gtk_clipboard, + 'qt': init_qt_clipboard, # TODO - split this into 'qtpy', 'pyqt4', and 'pyqt5' + 'xclip': init_xclip_clipboard, + 'xsel': init_xsel_clipboard, + 'klipper': init_klipper_clipboard, + 'windows': init_windows_clipboard, + 'no': init_no_clipboard} + if clipboard not in clipboard_types: + raise ValueError('Argument must be one of %s' % (', '.join([repr(_) for _ in clipboard_types.keys()]))) + + # Sets pyperclip's copy() and paste() functions: copy, paste = clipboard_types[clipboard]() -copy, paste = determine_clipboard() +def lazy_load_stub_copy(text): + ''' + A stub function for copy(), which will load the real copy() function when + called so that the real copy() function is used for later calls. -__all__ = ["copy", "paste"] + This allows users to import pyperclip without having determine_clipboard() + automatically run, which will automatically select a clipboard mechanism. + This could be a problem if it selects, say, the memory-heavy PyQt4 module + but the user was just going to immediately call set_clipboard() to use a + different clipboard mechanism. + The lazy loading this stub function implements gives the user a chance to + call set_clipboard() to pick another clipboard mechanism. Or, if the user + simply calls copy() or paste() without calling set_clipboard() first, + will fall back on whatever clipboard mechanism that determine_clipboard() + automatically chooses. + ''' + global copy, paste + copy, paste = determine_clipboard() + return copy(text) + + +def lazy_load_stub_paste(): + ''' + A stub function for paste(), which will load the real paste() function when + called so that the real paste() function is used for later calls. + + This allows users to import pyperclip without having determine_clipboard() + automatically run, which will automatically select a clipboard mechanism. + This could be a problem if it selects, say, the memory-heavy PyQt4 module + but the user was just going to immediately call set_clipboard() to use a + different clipboard mechanism. + + The lazy loading this stub function implements gives the user a chance to + call set_clipboard() to pick another clipboard mechanism. Or, if the user + simply calls copy() or paste() without calling set_clipboard() first, + will fall back on whatever clipboard mechanism that determine_clipboard() + automatically chooses. + ''' + global copy, paste + copy, paste = determine_clipboard() + return paste() + + +def is_available(): + return copy != lazy_load_stub_copy and paste != lazy_load_stub_paste + + +# Initially, copy() and paste() are set to lazy loading wrappers which will +# set `copy` and `paste` to real functions the first time they're used, unless +# set_clipboard() or determine_clipboard() is called first. +copy, paste = lazy_load_stub_copy, lazy_load_stub_paste + + +__all__ = ['copy', 'paste', 'set_clipboard', 'determine_clipboard'] # pandas aliases clipboard_get = paste clipboard_set = copy + diff --git a/pandas/io/clipboard/clipboards.py b/pandas/io/clipboard/clipboards.py deleted file mode 100644 index cb4ed8ed549d0..0000000000000 --- a/pandas/io/clipboard/clipboards.py +++ /dev/null @@ -1,129 +0,0 @@ -import subprocess - -from .exceptions import PyperclipException - -EXCEPT_MSG = """ - Pyperclip could not find a copy/paste mechanism for your system. - For more information, please visit https://pyperclip.readthedocs.org """ - - -def init_osx_clipboard(): - def copy_osx(text): - p = subprocess.Popen(["pbcopy", "w"], stdin=subprocess.PIPE, close_fds=True) - p.communicate(input=text.encode("utf-8")) - - def paste_osx(): - p = subprocess.Popen(["pbpaste", "r"], stdout=subprocess.PIPE, close_fds=True) - stdout, stderr = p.communicate() - return stdout.decode("utf-8") - - return copy_osx, paste_osx - - -def init_qt_clipboard(): - # $DISPLAY should exist - - # Try to import from qtpy, but if that fails try PyQt5 then PyQt4 - try: - from qtpy.QtWidgets import QApplication - except ImportError: - try: - from PyQt5.QtWidgets import QApplication - except ImportError: - from PyQt4.QtGui import QApplication - - app = QApplication.instance() - if app is None: - app = QApplication([]) - - def copy_qt(text): - cb = app.clipboard() - cb.setText(text) - - def paste_qt(): - cb = app.clipboard() - return str(cb.text()) - - return copy_qt, paste_qt - - -def init_xclip_clipboard(): - def copy_xclip(text): - p = subprocess.Popen( - ["xclip", "-selection", "c"], stdin=subprocess.PIPE, close_fds=True - ) - p.communicate(input=text.encode("utf-8")) - - def paste_xclip(): - p = subprocess.Popen( - ["xclip", "-selection", "c", "-o"], stdout=subprocess.PIPE, close_fds=True - ) - stdout, stderr = p.communicate() - return stdout.decode("utf-8") - - return copy_xclip, paste_xclip - - -def init_xsel_clipboard(): - def copy_xsel(text): - p = subprocess.Popen( - ["xsel", "-b", "-i"], stdin=subprocess.PIPE, close_fds=True - ) - p.communicate(input=text.encode("utf-8")) - - def paste_xsel(): - p = subprocess.Popen( - ["xsel", "-b", "-o"], stdout=subprocess.PIPE, close_fds=True - ) - stdout, stderr = p.communicate() - return stdout.decode("utf-8") - - return copy_xsel, paste_xsel - - -def init_klipper_clipboard(): - def copy_klipper(text): - p = subprocess.Popen( - [ - "qdbus", - "org.kde.klipper", - "/klipper", - "setClipboardContents", - text.encode("utf-8"), - ], - stdin=subprocess.PIPE, - close_fds=True, - ) - p.communicate(input=None) - - def paste_klipper(): - p = subprocess.Popen( - ["qdbus", "org.kde.klipper", "/klipper", "getClipboardContents"], - stdout=subprocess.PIPE, - close_fds=True, - ) - stdout, stderr = p.communicate() - - # Workaround for https://bugs.kde.org/show_bug.cgi?id=342874 - # TODO: https://github.com/asweigart/pyperclip/issues/43 - clipboardContents = stdout.decode("utf-8") - # even if blank, Klipper will append a newline at the end - assert len(clipboardContents) > 0 - # make sure that newline is there - assert clipboardContents.endswith("\n") - if clipboardContents.endswith("\n"): - clipboardContents = clipboardContents[:-1] - return clipboardContents - - return copy_klipper, paste_klipper - - -def init_no_clipboard(): - class ClipboardUnavailable: - def __call__(self, *args, **kwargs): - raise PyperclipException(EXCEPT_MSG) - - def __bool__(self): - return False - - return ClipboardUnavailable(), ClipboardUnavailable() diff --git a/pandas/io/clipboard/exceptions.py b/pandas/io/clipboard/exceptions.py deleted file mode 100644 index eaf5578b5cd1b..0000000000000 --- a/pandas/io/clipboard/exceptions.py +++ /dev/null @@ -1,11 +0,0 @@ -import ctypes - - -class PyperclipException(RuntimeError): - pass - - -class PyperclipWindowsException(PyperclipException): - def __init__(self, message): - message += " ({err})".format(err=ctypes.WinError()) - super().__init__(message) diff --git a/pandas/io/clipboard/windows.py b/pandas/io/clipboard/windows.py deleted file mode 100644 index 2935dfdc2ae19..0000000000000 --- a/pandas/io/clipboard/windows.py +++ /dev/null @@ -1,184 +0,0 @@ -""" -This module implements clipboard handling on Windows using ctypes. -""" -import contextlib -import ctypes -from ctypes import c_size_t, c_wchar, c_wchar_p, get_errno, sizeof -import time - -from .exceptions import PyperclipWindowsException - - -class CheckedCall: - def __init__(self, f): - super().__setattr__("f", f) - - def __call__(self, *args): - ret = self.f(*args) - if not ret and get_errno(): - raise PyperclipWindowsException("Error calling " + self.f.__name__) - return ret - - def __setattr__(self, key, value): - setattr(self.f, key, value) - - -def init_windows_clipboard(): - from ctypes.wintypes import ( - HGLOBAL, - LPVOID, - DWORD, - LPCSTR, - INT, - HWND, - HINSTANCE, - HMENU, - BOOL, - UINT, - HANDLE, - ) - - windll = ctypes.windll - msvcrt = ctypes.CDLL("msvcrt") - - safeCreateWindowExA = CheckedCall(windll.user32.CreateWindowExA) - safeCreateWindowExA.argtypes = [ - DWORD, - LPCSTR, - LPCSTR, - DWORD, - INT, - INT, - INT, - INT, - HWND, - HMENU, - HINSTANCE, - LPVOID, - ] - safeCreateWindowExA.restype = HWND - - safeDestroyWindow = CheckedCall(windll.user32.DestroyWindow) - safeDestroyWindow.argtypes = [HWND] - safeDestroyWindow.restype = BOOL - - OpenClipboard = windll.user32.OpenClipboard - OpenClipboard.argtypes = [HWND] - OpenClipboard.restype = BOOL - - safeCloseClipboard = CheckedCall(windll.user32.CloseClipboard) - safeCloseClipboard.argtypes = [] - safeCloseClipboard.restype = BOOL - - safeEmptyClipboard = CheckedCall(windll.user32.EmptyClipboard) - safeEmptyClipboard.argtypes = [] - safeEmptyClipboard.restype = BOOL - - safeGetClipboardData = CheckedCall(windll.user32.GetClipboardData) - safeGetClipboardData.argtypes = [UINT] - safeGetClipboardData.restype = HANDLE - - safeSetClipboardData = CheckedCall(windll.user32.SetClipboardData) - safeSetClipboardData.argtypes = [UINT, HANDLE] - safeSetClipboardData.restype = HANDLE - - safeGlobalAlloc = CheckedCall(windll.kernel32.GlobalAlloc) - safeGlobalAlloc.argtypes = [UINT, c_size_t] - safeGlobalAlloc.restype = HGLOBAL - - safeGlobalLock = CheckedCall(windll.kernel32.GlobalLock) - safeGlobalLock.argtypes = [HGLOBAL] - safeGlobalLock.restype = LPVOID - - safeGlobalUnlock = CheckedCall(windll.kernel32.GlobalUnlock) - safeGlobalUnlock.argtypes = [HGLOBAL] - safeGlobalUnlock.restype = BOOL - - wcslen = CheckedCall(msvcrt.wcslen) - wcslen.argtypes = [c_wchar_p] - wcslen.restype = UINT - - GMEM_MOVEABLE = 0x0002 - CF_UNICODETEXT = 13 - - @contextlib.contextmanager - def window(): - """ - Context that provides a valid Windows hwnd. - """ - # we really just need the hwnd, so setting "STATIC" - # as predefined lpClass is just fine. - hwnd = safeCreateWindowExA( - 0, b"STATIC", None, 0, 0, 0, 0, 0, None, None, None, None - ) - try: - yield hwnd - finally: - safeDestroyWindow(hwnd) - - @contextlib.contextmanager - def clipboard(hwnd): - """ - Context manager that opens the clipboard and prevents - other applications from modifying the clipboard content. - """ - # We may not get the clipboard handle immediately because - # some other application is accessing it (?) - # We try for at least 500ms to get the clipboard. - t = time.time() + 0.5 - success = False - while time.time() < t: - success = OpenClipboard(hwnd) - if success: - break - time.sleep(0.01) - if not success: - raise PyperclipWindowsException("Error calling OpenClipboard") - - try: - yield - finally: - safeCloseClipboard() - - def copy_windows(text): - # This function is heavily based on - # http://msdn.com/ms649016#_win32_Copying_Information_to_the_Clipboard - with window() as hwnd: - # http://msdn.com/ms649048 - # If an application calls OpenClipboard with hwnd set to NULL, - # EmptyClipboard sets the clipboard owner to NULL; - # this causes SetClipboardData to fail. - # => We need a valid hwnd to copy something. - with clipboard(hwnd): - safeEmptyClipboard() - - if text: - # http://msdn.com/ms649051 - # If the hMem parameter identifies a memory object, - # the object must have been allocated using the - # function with the GMEM_MOVEABLE flag. - count = wcslen(text) + 1 - handle = safeGlobalAlloc(GMEM_MOVEABLE, count * sizeof(c_wchar)) - locked_handle = safeGlobalLock(handle) - - ctypes.memmove( - c_wchar_p(locked_handle), - c_wchar_p(text), - count * sizeof(c_wchar), - ) - - safeGlobalUnlock(handle) - safeSetClipboardData(CF_UNICODETEXT, handle) - - def paste_windows(): - with clipboard(None): - handle = safeGetClipboardData(CF_UNICODETEXT) - if not handle: - # GetClipboardData may return NULL with errno == NO_ERROR - # if the clipboard is empty. - # (Also, it may return a handle to an empty buffer, - # but technically that's not empty) - return "" - return c_wchar_p(handle).value - - return copy_windows, paste_windows From 91af512c2cc415fd80dad699fa8b418eda6b9bcf Mon Sep 17 00:00:00 2001 From: krey Date: Mon, 16 Sep 2019 12:51:14 -0400 Subject: [PATCH 2/8] CLN: Removed PY2 references --- pandas/io/clipboard/__init__.py | 21 +++++---------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/pandas/io/clipboard/__init__.py b/pandas/io/clipboard/__init__.py index ecacba3a93ae7..64812fb83994f 100644 --- a/pandas/io/clipboard/__init__.py +++ b/pandas/io/clipboard/__init__.py @@ -66,10 +66,6 @@ Pyperclip could not find a copy/paste mechanism for your system. For more information, please visit https://pyperclip.readthedocs.io/en/latest/introduction.html#not-implemented-error """ -PY2 = sys.version_info[0] == 2 - -STR_OR_UNICODE = unicode if PY2 else str # For paste(): Python 3 uses str, Python 2 uses unicode. - ENCODING = 'utf-8' # The "which" unix command finds where a command is. @@ -95,13 +91,10 @@ def __init__(self, message): def _stringifyText(text): - if PY2: - acceptedTypes = (unicode, str, int, float, bool) - else: - acceptedTypes = (str, int, float, bool) + acceptedTypes = (str, int, float, bool) if not isinstance(text, acceptedTypes): raise PyperclipException('only str, int, float, and bool values can be copied to the clipboard, not %s' % (text.__class__.__name__)) - return STR_OR_UNICODE(text) + return str(text) def init_osx_pbcopy_clipboard(): @@ -186,7 +179,7 @@ def copy_qt(text): def paste_qt(): cb = app.clipboard() - return STR_OR_UNICODE(cb.text()) + return str(cb.text()) return copy_qt, paste_qt @@ -300,12 +293,8 @@ class ClipboardUnavailable(object): def __call__(self, *args, **kwargs): raise PyperclipException(EXCEPT_MSG) - if PY2: - def __nonzero__(self): - return False - else: - def __bool__(self): - return False + def __bool__(self): + return False return ClipboardUnavailable(), ClipboardUnavailable() From da46244658dccfb8b51532869e1ad243f06a84e0 Mon Sep 17 00:00:00 2001 From: krey Date: Mon, 16 Sep 2019 13:14:27 -0400 Subject: [PATCH 3/8] STYLE: black, flake8, isort, cleaned up super() and inhertance from base object --- pandas/io/clipboard/__init__.py | 303 +++++++++++++++++++------------- 1 file changed, 181 insertions(+), 122 deletions(-) diff --git a/pandas/io/clipboard/__init__.py b/pandas/io/clipboard/__init__.py index 64812fb83994f..d1942478d373b 100644 --- a/pandas/io/clipboard/__init__.py +++ b/pandas/io/clipboard/__init__.py @@ -1,7 +1,8 @@ """ Pyperclip -A cross-platform clipboard module for Python, with copy & paste functions for plain text. +A cross-platform clipboard module for Python, +with copy & paste functions for plain text. By Al Sweigart al@inventwithpython.com BSD License @@ -43,20 +44,17 @@ Pyperclip into running them with whatever permissions the Python process has. """ -__version__ = '1.7.0' +__version__ = "1.7.0" import contextlib import ctypes +from ctypes import c_size_t, c_wchar, c_wchar_p, get_errno, sizeof import os import platform import subprocess -import sys import time import warnings -from ctypes import c_size_t, sizeof, c_wchar_p, get_errno, c_wchar - - # `import PyQt4` sys.exit()s if DISPLAY is not in the environment. # Thus, we need to detect the presence of $DISPLAY manually # and not load PyQt4 if it is absent. @@ -64,50 +62,57 @@ EXCEPT_MSG = """ Pyperclip could not find a copy/paste mechanism for your system. - For more information, please visit https://pyperclip.readthedocs.io/en/latest/introduction.html#not-implemented-error """ + For more information, please visit + https://pyperclip.readthedocs.io/en/latest/introduction.html#not-implemented-error + """ -ENCODING = 'utf-8' +ENCODING = "utf-8" # The "which" unix command finds where a command is. -if platform.system() == 'Windows': - WHICH_CMD = 'where' +if platform.system() == "Windows": + WHICH_CMD = "where" else: - WHICH_CMD = 'which' + WHICH_CMD = "which" -def _executable_exists(name): - return subprocess.call([WHICH_CMD, name], - stdout=subprocess.PIPE, stderr=subprocess.PIPE) == 0 +def _executable_exists(name): + return ( + subprocess.call( + [WHICH_CMD, name], stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) + == 0 + ) # Exceptions class PyperclipException(RuntimeError): pass + class PyperclipWindowsException(PyperclipException): def __init__(self, message): message += " (%s)" % ctypes.WinError() - super(PyperclipWindowsException, self).__init__(message) + super().__init__(message) def _stringifyText(text): acceptedTypes = (str, int, float, bool) if not isinstance(text, acceptedTypes): - raise PyperclipException('only str, int, float, and bool values can be copied to the clipboard, not %s' % (text.__class__.__name__)) + raise PyperclipException( + "only str, int, float, and bool values" + "can be copied to the clipboard, not".format(text.__class__.__name__) + ) return str(text) def init_osx_pbcopy_clipboard(): - def copy_osx_pbcopy(text): - text = _stringifyText(text) # Converts non-str values to str. - p = subprocess.Popen(['pbcopy', 'w'], - stdin=subprocess.PIPE, close_fds=True) + text = _stringifyText(text) # Converts non-str values to str. + p = subprocess.Popen(["pbcopy", "w"], stdin=subprocess.PIPE, close_fds=True) p.communicate(input=text.encode(ENCODING)) def paste_osx_pbcopy(): - p = subprocess.Popen(['pbpaste', 'r'], - stdout=subprocess.PIPE, close_fds=True) + p = subprocess.Popen(["pbpaste", "r"], stdout=subprocess.PIPE, close_fds=True) stdout, stderr = p.communicate() return stdout.decode(ENCODING) @@ -116,8 +121,8 @@ def paste_osx_pbcopy(): def init_osx_pyobjc_clipboard(): def copy_osx_pyobjc(text): - '''Copy string argument to clipboard''' - text = _stringifyText(text) # Converts non-str values to str. + """Copy string argument to clipboard""" + text = _stringifyText(text) # Converts non-str values to str. newStr = Foundation.NSString.stringWithString_(text).nsstring() newData = newStr.dataUsingEncoding_(Foundation.NSUTF8StringEncoding) board = AppKit.NSPasteboard.generalPasteboard() @@ -139,7 +144,7 @@ def init_gtk_clipboard(): def copy_gtk(text): global cb - text = _stringifyText(text) # Converts non-str values to str. + text = _stringifyText(text) # Converts non-str values to str. cb = gtk.Clipboard() cb.set_text(text) cb.store() @@ -148,7 +153,7 @@ def paste_gtk(): clipboardContents = gtk.Clipboard().wait_for_text() # for python 2, returns None if the clipboard is blank. if clipboardContents is None: - return '' + return "" else: return clipboardContents @@ -162,10 +167,10 @@ def init_qt_clipboard(): # Try to import from qtpy, but if that fails try PyQt5 then PyQt4 try: from qtpy.QtWidgets import QApplication - except: + except ImportError: try: from PyQt5.QtWidgets import QApplication - except: + except ImportError: from PyQt4.QtGui import QApplication app = QApplication.instance() @@ -173,7 +178,7 @@ def init_qt_clipboard(): app = QApplication([]) def copy_qt(text): - text = _stringifyText(text) # Converts non-str values to str. + text = _stringifyText(text) # Converts non-str values to str. cb = app.clipboard() cb.setText(text) @@ -185,26 +190,29 @@ def paste_qt(): def init_xclip_clipboard(): - DEFAULT_SELECTION='c' - PRIMARY_SELECTION='p' + DEFAULT_SELECTION = "c" + PRIMARY_SELECTION = "p" def copy_xclip(text, primary=False): - text = _stringifyText(text) # Converts non-str values to str. - selection=DEFAULT_SELECTION + text = _stringifyText(text) # Converts non-str values to str. + selection = DEFAULT_SELECTION if primary: - selection=PRIMARY_SELECTION - p = subprocess.Popen(['xclip', '-selection', selection], - stdin=subprocess.PIPE, close_fds=True) + selection = PRIMARY_SELECTION + p = subprocess.Popen( + ["xclip", "-selection", selection], stdin=subprocess.PIPE, close_fds=True + ) p.communicate(input=text.encode(ENCODING)) def paste_xclip(primary=False): - selection=DEFAULT_SELECTION + selection = DEFAULT_SELECTION if primary: - selection=PRIMARY_SELECTION - p = subprocess.Popen(['xclip', '-selection', selection, '-o'], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - close_fds=True) + selection = PRIMARY_SELECTION + p = subprocess.Popen( + ["xclip", "-selection", selection, "-o"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + close_fds=True, + ) stdout, stderr = p.communicate() # Intentionally ignore extraneous output on stderr when clipboard is empty return stdout.decode(ENCODING) @@ -213,24 +221,26 @@ def paste_xclip(primary=False): def init_xsel_clipboard(): - DEFAULT_SELECTION='-b' - PRIMARY_SELECTION='-p' + DEFAULT_SELECTION = "-b" + PRIMARY_SELECTION = "-p" def copy_xsel(text, primary=False): - text = _stringifyText(text) # Converts non-str values to str. + text = _stringifyText(text) # Converts non-str values to str. selection_flag = DEFAULT_SELECTION if primary: selection_flag = PRIMARY_SELECTION - p = subprocess.Popen(['xsel', selection_flag, '-i'], - stdin=subprocess.PIPE, close_fds=True) + p = subprocess.Popen( + ["xsel", selection_flag, "-i"], stdin=subprocess.PIPE, close_fds=True + ) p.communicate(input=text.encode(ENCODING)) def paste_xsel(primary=False): selection_flag = DEFAULT_SELECTION if primary: selection_flag = PRIMARY_SELECTION - p = subprocess.Popen(['xsel', selection_flag, '-o'], - stdout=subprocess.PIPE, close_fds=True) + p = subprocess.Popen( + ["xsel", selection_flag, "-o"], stdout=subprocess.PIPE, close_fds=True + ) stdout, stderr = p.communicate() return stdout.decode(ENCODING) @@ -239,17 +249,26 @@ def paste_xsel(primary=False): def init_klipper_clipboard(): def copy_klipper(text): - text = _stringifyText(text) # Converts non-str values to str. + text = _stringifyText(text) # Converts non-str values to str. p = subprocess.Popen( - ['qdbus', 'org.kde.klipper', '/klipper', 'setClipboardContents', - text.encode(ENCODING)], - stdin=subprocess.PIPE, close_fds=True) + [ + "qdbus", + "org.kde.klipper", + "/klipper", + "setClipboardContents", + text.encode(ENCODING), + ], + stdin=subprocess.PIPE, + close_fds=True, + ) p.communicate(input=None) def paste_klipper(): p = subprocess.Popen( - ['qdbus', 'org.kde.klipper', '/klipper', 'getClipboardContents'], - stdout=subprocess.PIPE, close_fds=True) + ["qdbus", "org.kde.klipper", "/klipper", "getClipboardContents"], + stdout=subprocess.PIPE, + close_fds=True, + ) stdout, stderr = p.communicate() # Workaround for https://bugs.kde.org/show_bug.cgi?id=342874 @@ -258,8 +277,8 @@ def paste_klipper(): # even if blank, Klipper will append a newline at the end assert len(clipboardContents) > 0 # make sure that newline is there - assert clipboardContents.endswith('\n') - if clipboardContents.endswith('\n'): + assert clipboardContents.endswith("\n") + if clipboardContents.endswith("\n"): clipboardContents = clipboardContents[:-1] return clipboardContents @@ -268,18 +287,21 @@ def paste_klipper(): def init_dev_clipboard_clipboard(): def copy_dev_clipboard(text): - text = _stringifyText(text) # Converts non-str values to str. - if text == '': - warnings.warn('Pyperclip cannot copy a blank string to the clipboard on Cygwin. This is effectively a no-op.') - if '\r' in text: - warnings.warn('Pyperclip cannot handle \\r characters on Cygwin.') - - fo = open('/dev/clipboard', 'wt') + text = _stringifyText(text) # Converts non-str values to str. + if text == "": + warnings.warn( + "Pyperclip cannot copy a blank string to the clipboard on Cygwin." + "This is effectively a no-op." + ) + if "\r" in text: + warnings.warn("Pyperclip cannot handle \\r characters on Cygwin.") + + fo = open("/dev/clipboard", "wt") fo.write(text) fo.close() def paste_dev_clipboard(): - fo = open('/dev/clipboard', 'rt') + fo = open("/dev/clipboard", "rt") content = fo.read() fo.close() return content @@ -288,8 +310,7 @@ def paste_dev_clipboard(): def init_no_clipboard(): - class ClipboardUnavailable(object): - + class ClipboardUnavailable: def __call__(self, *args, **kwargs): raise PyperclipException(EXCEPT_MSG) @@ -299,12 +320,10 @@ def __bool__(self): return ClipboardUnavailable(), ClipboardUnavailable() - - # Windows-related clipboard functions: -class CheckedCall(object): +class CheckedCall: def __init__(self, f): - super(CheckedCall, self).__setattr__("f", f) + super().__setattr__("f", f) def __call__(self, *args): ret = self.f(*args) @@ -317,16 +336,40 @@ def __setattr__(self, key, value): def init_windows_clipboard(): - global HGLOBAL, LPVOID, DWORD, LPCSTR, INT, HWND, HINSTANCE, HMENU, BOOL, UINT, HANDLE - from ctypes.wintypes import (HGLOBAL, LPVOID, DWORD, LPCSTR, INT, HWND, - HINSTANCE, HMENU, BOOL, UINT, HANDLE) + global HGLOBAL, LPVOID, DWORD, LPCSTR, INT + global HWND, HINSTANCE, HMENU, BOOL, UINT, HANDLE + from ctypes.wintypes import ( + HGLOBAL, + LPVOID, + DWORD, + LPCSTR, + INT, + HWND, + HINSTANCE, + HMENU, + BOOL, + UINT, + HANDLE, + ) windll = ctypes.windll - msvcrt = ctypes.CDLL('msvcrt') + msvcrt = ctypes.CDLL("msvcrt") safeCreateWindowExA = CheckedCall(windll.user32.CreateWindowExA) - safeCreateWindowExA.argtypes = [DWORD, LPCSTR, LPCSTR, DWORD, INT, INT, - INT, INT, HWND, HMENU, HINSTANCE, LPVOID] + safeCreateWindowExA.argtypes = [ + DWORD, + LPCSTR, + LPCSTR, + DWORD, + INT, + INT, + INT, + INT, + HWND, + HMENU, + HINSTANCE, + LPVOID, + ] safeCreateWindowExA.restype = HWND safeDestroyWindow = CheckedCall(windll.user32.DestroyWindow) @@ -379,8 +422,9 @@ def window(): """ # we really just need the hwnd, so setting "STATIC" # as predefined lpClass is just fine. - hwnd = safeCreateWindowExA(0, b"STATIC", None, 0, 0, 0, 0, 0, - None, None, None, None) + hwnd = safeCreateWindowExA( + 0, b"STATIC", None, 0, 0, 0, 0, 0, None, None, None, None + ) try: yield hwnd finally: @@ -414,7 +458,7 @@ def copy_windows(text): # This function is heavily based on # http://msdn.com/ms649016#_win32_Copying_Information_to_the_Clipboard - text = _stringifyText(text) # Converts non-str values to str. + text = _stringifyText(text) # Converts non-str values to str. with window() as hwnd: # http://msdn.com/ms649048 @@ -431,11 +475,14 @@ def copy_windows(text): # the object must have been allocated using the # function with the GMEM_MOVEABLE flag. count = wcslen(text) + 1 - handle = safeGlobalAlloc(GMEM_MOVEABLE, - count * sizeof(c_wchar)) + handle = safeGlobalAlloc(GMEM_MOVEABLE, count * sizeof(c_wchar)) locked_handle = safeGlobalLock(handle) - ctypes.memmove(c_wchar_p(locked_handle), c_wchar_p(text), count * sizeof(c_wchar)) + ctypes.memmove( + c_wchar_p(locked_handle), + c_wchar_p(text), + count * sizeof(c_wchar), + ) safeGlobalUnlock(handle) safeSetClipboardData(CF_UNICODETEXT, handle) @@ -456,16 +503,17 @@ def paste_windows(): def init_wsl_clipboard(): def copy_wsl(text): - text = _stringifyText(text) # Converts non-str values to str. - p = subprocess.Popen(['clip.exe'], - stdin=subprocess.PIPE, close_fds=True) + text = _stringifyText(text) # Converts non-str values to str. + p = subprocess.Popen(["clip.exe"], stdin=subprocess.PIPE, close_fds=True) p.communicate(input=text.encode(ENCODING)) def paste_wsl(): - p = subprocess.Popen(['powershell.exe', '-command', 'Get-Clipboard'], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - close_fds=True) + p = subprocess.Popen( + ["powershell.exe", "-command", "Get-Clipboard"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + close_fds=True, + ) stdout, stderr = p.communicate() # WSL appends "\r\n" to the contents. return stdout[:-2].decode(ENCODING) @@ -473,34 +521,41 @@ def paste_wsl(): return copy_wsl, paste_wsl -# Automatic detection of clipboard mechanisms and importing is done in deteremine_clipboard(): +# Automatic detection of clipboard mechanisms +# and importing is done in deteremine_clipboard(): def determine_clipboard(): - ''' + """ Determine the OS/platform and set the copy() and paste() functions accordingly. - ''' + """ global Foundation, AppKit, gtk, qtpy, PyQt4, PyQt5 # Setup for the CYGWIN platform: - if 'cygwin' in platform.system().lower(): # Cygwin has a variety of values returned by platform.system(), such as 'CYGWIN_NT-6.1' + if ( + "cygwin" in platform.system().lower() + ): # Cygwin has a variety of values returned by platform.system(), + # such as 'CYGWIN_NT-6.1' # FIXME: pyperclip currently does not support Cygwin, # see https://github.com/asweigart/pyperclip/issues/55 - if os.path.exists('/dev/clipboard'): - warnings.warn('Pyperclip\'s support for Cygwin is not perfect, see https://github.com/asweigart/pyperclip/issues/55') + if os.path.exists("/dev/clipboard"): + warnings.warn( + "Pyperclip's support for Cygwin is not perfect," + "see https://github.com/asweigart/pyperclip/issues/55" + ) return init_dev_clipboard_clipboard() # Setup for the WINDOWS platform: - elif os.name == 'nt' or platform.system() == 'Windows': + elif os.name == "nt" or platform.system() == "Windows": return init_windows_clipboard() - if platform.system() == 'Linux': - with open('/proc/version', 'r') as f: + if platform.system() == "Linux": + with open("/proc/version", "r") as f: if "Microsoft" in f.read(): return init_wsl_clipboard() # Setup for the MAC OS X platform: - if os.name == 'mac' or platform.system() == 'Darwin': + if os.name == "mac" or platform.system() == "Darwin": try: import Foundation # check if pyobjc is installed import AppKit @@ -514,7 +569,7 @@ def determine_clipboard(): try: import gtk # check if gtk is installed except ImportError: - pass # We want to fail fast for all non-ImportError exceptions. + pass # We want to fail fast for all non-ImportError exceptions. else: return init_gtk_clipboard() @@ -526,7 +581,8 @@ def determine_clipboard(): return init_klipper_clipboard() try: - # qtpy is a small abstraction layer that lets you write applications using a single api call to either PyQt or PySide. + # qtpy is a small abstraction layer that lets you write applications + # using a single api call to either PyQt or PySide. # https://pypi.python.org/pypi/QtPy import qtpy # check if qtpy is installed except ImportError: @@ -537,7 +593,7 @@ def determine_clipboard(): try: import PyQt4 # check if PyQt4 is installed except ImportError: - pass # We want to fail fast for all non-ImportError exceptions. + pass # We want to fail fast for all non-ImportError exceptions. else: return init_qt_clipboard() else: @@ -545,12 +601,11 @@ def determine_clipboard(): else: return init_qt_clipboard() - return init_no_clipboard() def set_clipboard(clipboard): - ''' + """ Explicitly sets the clipboard mechanism. The "clipboard mechanism" is how the copy() and paste() functions interact with the operating system to implement the copy/paste feature. The clipboard parameter must be one of: @@ -563,28 +618,33 @@ def set_clipboard(clipboard): - klipper - windows (default on Windows) - no (this is what is set when no clipboard mechanism can be found) - ''' + """ global copy, paste - clipboard_types = {'pbcopy': init_osx_pbcopy_clipboard, - 'pyobjc': init_osx_pyobjc_clipboard, - 'gtk': init_gtk_clipboard, - 'qt': init_qt_clipboard, # TODO - split this into 'qtpy', 'pyqt4', and 'pyqt5' - 'xclip': init_xclip_clipboard, - 'xsel': init_xsel_clipboard, - 'klipper': init_klipper_clipboard, - 'windows': init_windows_clipboard, - 'no': init_no_clipboard} + clipboard_types = { + "pbcopy": init_osx_pbcopy_clipboard, + "pyobjc": init_osx_pyobjc_clipboard, + "gtk": init_gtk_clipboard, + "qt": init_qt_clipboard, # TODO - split this into 'qtpy', 'pyqt4', and 'pyqt5' + "xclip": init_xclip_clipboard, + "xsel": init_xsel_clipboard, + "klipper": init_klipper_clipboard, + "windows": init_windows_clipboard, + "no": init_no_clipboard, + } if clipboard not in clipboard_types: - raise ValueError('Argument must be one of %s' % (', '.join([repr(_) for _ in clipboard_types.keys()]))) + raise ValueError( + "Argument must be one of %s" + % (", ".join([repr(_) for _ in clipboard_types.keys()])) + ) # Sets pyperclip's copy() and paste() functions: copy, paste = clipboard_types[clipboard]() def lazy_load_stub_copy(text): - ''' + """ A stub function for copy(), which will load the real copy() function when called so that the real copy() function is used for later calls. @@ -599,14 +659,14 @@ def lazy_load_stub_copy(text): simply calls copy() or paste() without calling set_clipboard() first, will fall back on whatever clipboard mechanism that determine_clipboard() automatically chooses. - ''' + """ global copy, paste copy, paste = determine_clipboard() return copy(text) def lazy_load_stub_paste(): - ''' + """ A stub function for paste(), which will load the real paste() function when called so that the real paste() function is used for later calls. @@ -621,7 +681,7 @@ def lazy_load_stub_paste(): simply calls copy() or paste() without calling set_clipboard() first, will fall back on whatever clipboard mechanism that determine_clipboard() automatically chooses. - ''' + """ global copy, paste copy, paste = determine_clipboard() return paste() @@ -637,9 +697,8 @@ def is_available(): copy, paste = lazy_load_stub_copy, lazy_load_stub_paste -__all__ = ['copy', 'paste', 'set_clipboard', 'determine_clipboard'] +__all__ = ["copy", "paste", "set_clipboard", "determine_clipboard"] # pandas aliases clipboard_get = paste clipboard_set = copy - From fa393469aba9dbec850266a1c0a1e8af29d2c7b8 Mon Sep 17 00:00:00 2001 From: krey Date: Mon, 16 Sep 2019 13:24:35 -0400 Subject: [PATCH 4/8] DOC: Link to new PyPI instead of legacy --- pandas/io/clipboard/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/io/clipboard/__init__.py b/pandas/io/clipboard/__init__.py index d1942478d373b..466ad21683eaf 100644 --- a/pandas/io/clipboard/__init__.py +++ b/pandas/io/clipboard/__init__.py @@ -583,7 +583,7 @@ def determine_clipboard(): try: # qtpy is a small abstraction layer that lets you write applications # using a single api call to either PyQt or PySide. - # https://pypi.python.org/pypi/QtPy + # https://pypi.python.org/project/QtPy import qtpy # check if qtpy is installed except ImportError: # If qtpy isn't installed, fall back on importing PyQt4. From 268028ee0d00fe19a361ae28d0d3f75ab5ba6ae9 Mon Sep 17 00:00:00 2001 From: krey Date: Mon, 16 Sep 2019 18:54:08 -0400 Subject: [PATCH 5/8] Fixed clipboard tests. --- pandas/tests/io/test_clipboard.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pandas/tests/io/test_clipboard.py b/pandas/tests/io/test_clipboard.py index 33e6d3b05100e..4559ba264d8b7 100644 --- a/pandas/tests/io/test_clipboard.py +++ b/pandas/tests/io/test_clipboard.py @@ -8,8 +8,7 @@ from pandas import DataFrame, get_option, read_clipboard import pandas.util.testing as tm -from pandas.io.clipboard import clipboard_get, clipboard_set -from pandas.io.clipboard.exceptions import PyperclipException +from pandas.io.clipboard import PyperclipException, clipboard_get, clipboard_set try: DataFrame({"A": [1, 2]}).to_clipboard() From 003e08eb896655248e0d554d15c2f117cbe59fe1 Mon Sep 17 00:00:00 2001 From: krey Date: Mon, 23 Sep 2019 21:13:16 -0400 Subject: [PATCH 6/8] Removed outdated gtk package from code --- pandas/io/clipboard/__init__.py | 41 +++------------------------------ 1 file changed, 3 insertions(+), 38 deletions(-) diff --git a/pandas/io/clipboard/__init__.py b/pandas/io/clipboard/__init__.py index 466ad21683eaf..63dd40a229dfc 100644 --- a/pandas/io/clipboard/__init__.py +++ b/pandas/io/clipboard/__init__.py @@ -21,13 +21,9 @@ sudo apt-get install xclip sudo apt-get install xsel -Otherwise on Linux, you will need the gtk or PyQt5/PyQt4 modules installed. +Otherwise on Linux, you will need the PyQt5 modules installed. -gtk and PyQt4 modules are not available for Python 3, -and this module does not work with PyGObject yet. - -Note: There seems to be a way to get gtk on Python 3, according to: - https://askubuntu.com/questions/697397/python3-is-not-supporting-gtk-module +This module does not work with PyGObject yet. Cygwin is currently not supported. @@ -138,28 +134,6 @@ def paste_osx_pyobjc(): return copy_osx_pyobjc, paste_osx_pyobjc -def init_gtk_clipboard(): - global gtk - import gtk - - def copy_gtk(text): - global cb - text = _stringifyText(text) # Converts non-str values to str. - cb = gtk.Clipboard() - cb.set_text(text) - cb.store() - - def paste_gtk(): - clipboardContents = gtk.Clipboard().wait_for_text() - # for python 2, returns None if the clipboard is blank. - if clipboardContents is None: - return "" - else: - return clipboardContents - - return copy_gtk, paste_gtk - - def init_qt_clipboard(): global QApplication # $DISPLAY should exist @@ -529,7 +503,7 @@ def determine_clipboard(): accordingly. """ - global Foundation, AppKit, gtk, qtpy, PyQt4, PyQt5 + global Foundation, AppKit, qtpy, PyQt4, PyQt5 # Setup for the CYGWIN platform: if ( @@ -566,13 +540,6 @@ def determine_clipboard(): # Setup for the LINUX platform: if HAS_DISPLAY: - try: - import gtk # check if gtk is installed - except ImportError: - pass # We want to fail fast for all non-ImportError exceptions. - else: - return init_gtk_clipboard() - if _executable_exists("xsel"): return init_xsel_clipboard() if _executable_exists("xclip"): @@ -611,7 +578,6 @@ def set_clipboard(clipboard): implement the copy/paste feature. The clipboard parameter must be one of: - pbcopy - pbobjc (default on Mac OS X) - - gtk - qt - xclip - xsel @@ -624,7 +590,6 @@ def set_clipboard(clipboard): clipboard_types = { "pbcopy": init_osx_pbcopy_clipboard, "pyobjc": init_osx_pyobjc_clipboard, - "gtk": init_gtk_clipboard, "qt": init_qt_clipboard, # TODO - split this into 'qtpy', 'pyqt4', and 'pyqt5' "xclip": init_xclip_clipboard, "xsel": init_xsel_clipboard, From 741dcfc01f844f8b0f8162e5828c4a3569550f68 Mon Sep 17 00:00:00 2001 From: krey Date: Mon, 21 Oct 2019 17:52:29 -0400 Subject: [PATCH 7/8] TST: added pyperclip's tests --- pandas/tests/io/test_pyperclip.py | 189 ++++++++++++++++++++++++++++++ 1 file changed, 189 insertions(+) create mode 100644 pandas/tests/io/test_pyperclip.py diff --git a/pandas/tests/io/test_pyperclip.py b/pandas/tests/io/test_pyperclip.py new file mode 100644 index 0000000000000..e3d1e6a48dd31 --- /dev/null +++ b/pandas/tests/io/test_pyperclip.py @@ -0,0 +1,189 @@ +# coding: utf-8 +import os +import platform +import random +import string +import unittest + +from pandas.io.clipboard import ( + HAS_DISPLAY, + PyperclipException, + _executable_exists, + init_dev_clipboard_clipboard, + init_klipper_clipboard, + init_no_clipboard, + init_osx_pbcopy_clipboard, + init_osx_pyobjc_clipboard, + init_qt_clipboard, + init_windows_clipboard, + init_wsl_clipboard, + init_xclip_clipboard, + init_xsel_clipboard, +) + +# import sys +# sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) + + +random.seed(42) # Make the "random" tests reproducible. + + +class _TestClipboard(unittest.TestCase): + clipboard = None + supports_unicode = True + + @property + def copy(self): + return self.clipboard[0] + + @property + def paste(self): + return self.clipboard[1] + + def setUp(self): + if not self.clipboard: + self.skipTest("Clipboard not supported.") + + def test_copy_simple(self): + self.copy("pyper\r\nclip") + + def test_copy_paste_simple(self): + msg = "".join( + random.choice(string.ascii_letters + string.digits) for _ in range(1000) + ) + self.copy(msg) + self.assertEqual(self.paste(), msg) + + def test_copy_paste_whitespace(self): + msg = "".join(random.choice(string.whitespace) for _ in range(1000)) + self.copy(msg) + self.assertEqual(self.paste(), msg) + + def test_copy_blank(self): + self.copy("TEST") + self.copy("") + self.assertEqual(self.paste(), "") + + def test_copy_unicode(self): + if not self.supports_unicode: + raise unittest.SkipTest() + self.copy(u"ಠ_ಠ") + + def test_copy_unicode_emoji(self): + if not self.supports_unicode: + raise unittest.SkipTest() + self.copy(u"🙆") + + def test_copy_paste_unicode(self): + if not self.supports_unicode: + raise unittest.SkipTest() + msg = u"ಠ_ಠ" + self.copy(msg) + self.assertEqual(self.paste(), msg) + + def test_copy_paste_unicode_emoji(self): + if not self.supports_unicode: + raise unittest.SkipTest() + msg = u"🙆" + self.copy(msg) + self.assertEqual(self.paste(), msg) + + def test_non_str(self): + # Test copying an int. + self.copy(42) + self.assertEqual(self.paste(), "42") + + self.copy(-1) + self.assertEqual(self.paste(), "-1") + + # Test copying a float. + self.copy(3.141592) + self.assertEqual(self.paste(), "3.141592") + + # Test copying bools. + self.copy(True) + self.assertEqual(self.paste(), "True") + + self.copy(False) + self.assertEqual(self.paste(), "False") + + # All other non-str values raise an exception. + with self.assertRaises(PyperclipException): + self.copy(None) + + with self.assertRaises(PyperclipException): + self.copy([2, 4, 6, 8]) + + +class TestCygwin(_TestClipboard): + if "cygwin" in platform.system().lower(): + clipboard = init_dev_clipboard_clipboard() + + +class TestWindows(_TestClipboard): + if os.name == "nt" or platform.system() == "Windows": + clipboard = init_windows_clipboard() + + +class TestWSL(_TestClipboard): + if platform.system() == "Linux": + with open("/proc/version", "r") as f: + if "Microsoft" in f.read(): + clipboard = init_wsl_clipboard() + + +class TestOSX(_TestClipboard): + if os.name == "mac" or platform.system() == "Darwin": + try: + import Foundation # check if pyobjc is installed + import AppKit + except ImportError: + clipboard = init_osx_pbcopy_clipboard() # TODO + else: + clipboard = init_osx_pyobjc_clipboard() + + +class TestQt(_TestClipboard): + if HAS_DISPLAY: + try: + import PyQt5 + except ImportError: + try: + import PyQt4 + except ImportError: + pass + else: + clipboard = init_qt_clipboard() + else: + clipboard = init_qt_clipboard() + + +class TestXClip(_TestClipboard): + if _executable_exists("xclip"): + clipboard = init_xclip_clipboard() + + +class TestXSel(_TestClipboard): + if _executable_exists("xsel"): + clipboard = init_xsel_clipboard() + + +class TestKlipper(_TestClipboard): + if _executable_exists("klipper") and _executable_exists("qdbus"): + clipboard = init_klipper_clipboard() + + +class TestNoClipboard(unittest.TestCase): + copy, paste = init_no_clipboard() + + def test_copy(self): + with self.assertRaises(RuntimeError): + self.copy("foo") + + def test_paste(self): + with self.assertRaises(RuntimeError): + self.paste() + + +if __name__ == "__main__": + unittest.main() From 4a51a4e0cff0111856ef3888ec41c2cfd65210c5 Mon Sep 17 00:00:00 2001 From: Will Ayd Date: Mon, 21 Oct 2019 19:00:11 -0700 Subject: [PATCH 8/8] Reverted test additions --- pandas/tests/io/test_pyperclip.py | 189 ------------------------------ 1 file changed, 189 deletions(-) delete mode 100644 pandas/tests/io/test_pyperclip.py diff --git a/pandas/tests/io/test_pyperclip.py b/pandas/tests/io/test_pyperclip.py deleted file mode 100644 index e3d1e6a48dd31..0000000000000 --- a/pandas/tests/io/test_pyperclip.py +++ /dev/null @@ -1,189 +0,0 @@ -# coding: utf-8 -import os -import platform -import random -import string -import unittest - -from pandas.io.clipboard import ( - HAS_DISPLAY, - PyperclipException, - _executable_exists, - init_dev_clipboard_clipboard, - init_klipper_clipboard, - init_no_clipboard, - init_osx_pbcopy_clipboard, - init_osx_pyobjc_clipboard, - init_qt_clipboard, - init_windows_clipboard, - init_wsl_clipboard, - init_xclip_clipboard, - init_xsel_clipboard, -) - -# import sys -# sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) - - -random.seed(42) # Make the "random" tests reproducible. - - -class _TestClipboard(unittest.TestCase): - clipboard = None - supports_unicode = True - - @property - def copy(self): - return self.clipboard[0] - - @property - def paste(self): - return self.clipboard[1] - - def setUp(self): - if not self.clipboard: - self.skipTest("Clipboard not supported.") - - def test_copy_simple(self): - self.copy("pyper\r\nclip") - - def test_copy_paste_simple(self): - msg = "".join( - random.choice(string.ascii_letters + string.digits) for _ in range(1000) - ) - self.copy(msg) - self.assertEqual(self.paste(), msg) - - def test_copy_paste_whitespace(self): - msg = "".join(random.choice(string.whitespace) for _ in range(1000)) - self.copy(msg) - self.assertEqual(self.paste(), msg) - - def test_copy_blank(self): - self.copy("TEST") - self.copy("") - self.assertEqual(self.paste(), "") - - def test_copy_unicode(self): - if not self.supports_unicode: - raise unittest.SkipTest() - self.copy(u"ಠ_ಠ") - - def test_copy_unicode_emoji(self): - if not self.supports_unicode: - raise unittest.SkipTest() - self.copy(u"🙆") - - def test_copy_paste_unicode(self): - if not self.supports_unicode: - raise unittest.SkipTest() - msg = u"ಠ_ಠ" - self.copy(msg) - self.assertEqual(self.paste(), msg) - - def test_copy_paste_unicode_emoji(self): - if not self.supports_unicode: - raise unittest.SkipTest() - msg = u"🙆" - self.copy(msg) - self.assertEqual(self.paste(), msg) - - def test_non_str(self): - # Test copying an int. - self.copy(42) - self.assertEqual(self.paste(), "42") - - self.copy(-1) - self.assertEqual(self.paste(), "-1") - - # Test copying a float. - self.copy(3.141592) - self.assertEqual(self.paste(), "3.141592") - - # Test copying bools. - self.copy(True) - self.assertEqual(self.paste(), "True") - - self.copy(False) - self.assertEqual(self.paste(), "False") - - # All other non-str values raise an exception. - with self.assertRaises(PyperclipException): - self.copy(None) - - with self.assertRaises(PyperclipException): - self.copy([2, 4, 6, 8]) - - -class TestCygwin(_TestClipboard): - if "cygwin" in platform.system().lower(): - clipboard = init_dev_clipboard_clipboard() - - -class TestWindows(_TestClipboard): - if os.name == "nt" or platform.system() == "Windows": - clipboard = init_windows_clipboard() - - -class TestWSL(_TestClipboard): - if platform.system() == "Linux": - with open("/proc/version", "r") as f: - if "Microsoft" in f.read(): - clipboard = init_wsl_clipboard() - - -class TestOSX(_TestClipboard): - if os.name == "mac" or platform.system() == "Darwin": - try: - import Foundation # check if pyobjc is installed - import AppKit - except ImportError: - clipboard = init_osx_pbcopy_clipboard() # TODO - else: - clipboard = init_osx_pyobjc_clipboard() - - -class TestQt(_TestClipboard): - if HAS_DISPLAY: - try: - import PyQt5 - except ImportError: - try: - import PyQt4 - except ImportError: - pass - else: - clipboard = init_qt_clipboard() - else: - clipboard = init_qt_clipboard() - - -class TestXClip(_TestClipboard): - if _executable_exists("xclip"): - clipboard = init_xclip_clipboard() - - -class TestXSel(_TestClipboard): - if _executable_exists("xsel"): - clipboard = init_xsel_clipboard() - - -class TestKlipper(_TestClipboard): - if _executable_exists("klipper") and _executable_exists("qdbus"): - clipboard = init_klipper_clipboard() - - -class TestNoClipboard(unittest.TestCase): - copy, paste = init_no_clipboard() - - def test_copy(self): - with self.assertRaises(RuntimeError): - self.copy("foo") - - def test_paste(self): - with self.assertRaises(RuntimeError): - self.paste() - - -if __name__ == "__main__": - unittest.main()