From ae76bbfee406ce87e02ad05f4bd214c96eb686e1 Mon Sep 17 00:00:00 2001 From: Ashutosh <37182127+ashu-tosh-kumar@users.noreply.github.com> Date: Thu, 2 Jan 2025 22:28:38 +0530 Subject: [PATCH] update pyperclip code to v1.9.0 and fix snyk issues --- lib/src/pyperclip/__init__.py | 272 +++++++++++++--------------------- 1 file changed, 103 insertions(+), 169 deletions(-) diff --git a/lib/src/pyperclip/__init__.py b/lib/src/pyperclip/__init__.py index a6cb447..e8b284c 100644 --- a/lib/src/pyperclip/__init__.py +++ b/lib/src/pyperclip/__init__.py @@ -22,19 +22,14 @@ sudo apt-get install xsel sudo apt-get install wl-clipboard -Otherwise on Linux, you will need the gtk or PyQt5/PyQt4 modules installed. +Otherwise on Linux, you will need the qtpy or 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. Security Note: This module runs programs with these names: - which - - where - pbcopy - pbpaste - xclip @@ -46,8 +41,9 @@ Pyperclip into running them with whatever permissions the Python process has. """ -__version__ = '1.8.2' +__version__ = '1.9.0' +import base64 import contextlib import ctypes import os @@ -56,39 +52,31 @@ import sys import time import warnings +from ctypes import c_size_t, c_wchar, c_wchar_p, get_errno, sizeof -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. -HAS_DISPLAY = os.getenv("DISPLAY", False) - -EXCEPT_MSG = """ - Pyperclip could not find a copy/paste mechanism for your system. - For more information, please visit https://pyperclip.readthedocs.io/en/latest/index.html#not-implemented-error """ +_IS_RUNNING_PYTHON_2 = sys.version_info[0] == 2 # type: bool -PY2 = sys.version_info[0] == 2 +# For paste(): Python 3 uses str, Python 2 uses unicode. +if _IS_RUNNING_PYTHON_2: + # mypy complains about `unicode` for Python 2, so we ignore the type error: + _PYTHON_STR_TYPE = unicode # noqa: F821 +else: + _PYTHON_STR_TYPE = str -STR_OR_UNICODE = unicode if PY2 else str # For paste(): Python 3 uses str, Python 2 uses unicode. - -ENCODING = 'utf-8' +ENCODING = 'utf-8' # type: str try: - from shutil import which as _executable_exists + # Use shutil.which() for Python 3+ + from shutil import which + def _py3_executable_exists(name): # type: (str) -> bool + return bool(which(name)) + _executable_exists = _py3_executable_exists except ImportError: - # 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([WHICH_CMD, name], + # Use the "which" unix command for Python 2.7 and prior. + def _py2_executable_exists(name): # type: (str) -> bool + return subprocess.Popen(['which', name], stdout=subprocess.PIPE, stderr=subprocess.PIPE) == 0 - - + _executable_exists = _py2_executable_exists # Exceptions class PyperclipException(RuntimeError): @@ -102,20 +90,10 @@ def __init__(self, message): class PyperclipTimeoutException(PyperclipException): pass -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. + text = _PYTHON_STR_TYPE(text) # Converts non-str values to str. p = subprocess.Popen(['pbcopy', 'w'], stdin=subprocess.PIPE, close_fds=True) p.communicate(input=text.encode(ENCODING)) @@ -132,7 +110,7 @@ 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. + text = _PYTHON_STR_TYPE(text) # Converts non-str values to str. newStr = Foundation.NSString.stringWithString_(text).nsstring() newData = newStr.dataUsingEncoding_(Foundation.NSUTF8StringEncoding) board = AppKit.NSPasteboard.generalPasteboard() @@ -148,53 +126,28 @@ 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 - # Try to import from qtpy, but if that fails try PyQt5 then PyQt4 + # Try to import from qtpy, but if that fails try PyQt5 try: from qtpy.QtWidgets import QApplication except: - try: - from PyQt5.QtWidgets import QApplication - except: - from PyQt4.QtGui import QApplication + from PyQt5.QtWidgets import QApplication app = QApplication.instance() if app is None: app = QApplication([]) def copy_qt(text): - text = _stringifyText(text) # Converts non-str values to str. + text = _PYTHON_STR_TYPE(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 _PYTHON_STR_TYPE(cb.text()) return copy_qt, paste_qt @@ -204,7 +157,7 @@ def init_xclip_clipboard(): PRIMARY_SELECTION='p' def copy_xclip(text, primary=False): - text = _stringifyText(text) # Converts non-str values to str. + text = _PYTHON_STR_TYPE(text) # Converts non-str values to str. selection=DEFAULT_SELECTION if primary: selection=PRIMARY_SELECTION @@ -232,7 +185,7 @@ def init_xsel_clipboard(): PRIMARY_SELECTION='-p' def copy_xsel(text, primary=False): - text = _stringifyText(text) # Converts non-str values to str. + text = _PYTHON_STR_TYPE(text) # Converts non-str values to str. selection_flag = DEFAULT_SELECTION if primary: selection_flag = PRIMARY_SELECTION @@ -256,7 +209,7 @@ def init_wl_clipboard(): PRIMARY_SELECTION = "-p" def copy_wl(text, primary=False): - text = _stringifyText(text) # Converts non-str values to str. + text = _PYTHON_STR_TYPE(text) # Converts non-str values to str. args = ["wl-copy"] if primary: args.append(PRIMARY_SELECTION) @@ -269,7 +222,7 @@ def copy_wl(text, primary=False): p.communicate(input=text.encode(ENCODING)) def paste_wl(primary=False): - args = ["wl-paste", "-n"] + args = ["wl-paste", "-n", "-t", "text"] if primary: args.append(PRIMARY_SELECTION) p = subprocess.Popen(args, stdout=subprocess.PIPE, close_fds=True) @@ -281,7 +234,7 @@ def paste_wl(primary=False): def init_klipper_clipboard(): def copy_klipper(text): - text = _stringifyText(text) # Converts non-str values to str. + text = _PYTHON_STR_TYPE(text) # Converts non-str values to str. p = subprocess.Popen( ['qdbus', 'org.kde.klipper', '/klipper', 'setClipboardContents', text.encode(ENCODING)], @@ -310,7 +263,7 @@ def paste_klipper(): def init_dev_clipboard_clipboard(): def copy_dev_clipboard(text): - text = _stringifyText(text) # Converts non-str values to str. + text = _PYTHON_STR_TYPE(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: @@ -333,12 +286,17 @@ def init_no_clipboard(): class ClipboardUnavailable(object): def __call__(self, *args, **kwargs): - raise PyperclipException(EXCEPT_MSG) + additionalInfo = '' + if sys.platform == 'linux': + additionalInfo = '\nOn Linux, you can run `sudo apt-get install xclip` or `sudo apt-get install xselect` to install a copy/paste mechanism.' + raise PyperclipException('Pyperclip could not find a copy/paste mechanism for your system. For more information, please visit https://pyperclip.readthedocs.io/en/latest/index.html#not-implemented-error' + additionalInfo) - if PY2: + if _IS_RUNNING_PYTHON_2: + # file deepcode ignore MissingParameter def __nonzero__(self): return False else: + # file deepcode ignore MissingParameter def __bool__(self): return False @@ -364,8 +322,19 @@ 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) + from ctypes.wintypes import ( + BOOL, + DWORD, + HANDLE, + HGLOBAL, + HINSTANCE, + HMENU, + HWND, + INT, + LPCSTR, + LPVOID, + UINT, + ) windll = ctypes.windll msvcrt = ctypes.CDLL('msvcrt') @@ -460,7 +429,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 = _PYTHON_STR_TYPE(text) # Converts non-str values to str. with window() as hwnd: # http://msdn.com/ms649048 @@ -495,38 +464,53 @@ def paste_windows(): # (Also, it may return a handle to an empty buffer, # but technically that's not empty) return "" - return c_wchar_p(handle).value + locked_handle = safeGlobalLock(handle) + return_value = c_wchar_p(locked_handle).value + safeGlobalUnlock(handle) + return return_value return copy_windows, paste_windows def init_wsl_clipboard(): + def copy_wsl(text): - text = _stringifyText(text) # Converts non-str values to str. + text = _PYTHON_STR_TYPE(text) # Converts non-str values to str. p = subprocess.Popen(['clip.exe'], stdin=subprocess.PIPE, close_fds=True) - p.communicate(input=text.encode(ENCODING)) + p.communicate(input=text.encode('utf-16le')) def paste_wsl(): - p = subprocess.Popen(['powershell.exe', '-command', 'Get-Clipboard'], + ps_script = '[Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes((Get-Clipboard -Raw)))' + + # '-noprofile' speeds up load time + p = subprocess.Popen(['powershell.exe', '-noprofile', '-command', ps_script], 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) + + if stderr: + raise Exception(f"Error pasting from clipboard: {stderr}") + + try: + base64_encoded = stdout.decode('utf-8').strip() + decoded_bytes = base64.b64decode(base64_encoded) + return decoded_bytes.decode('utf-8') + except Exception as e: + raise RuntimeError(f"Decoding error: {e}") 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 determine_clipboard(): def determine_clipboard(): ''' Determine the OS/platform and set the copy() and paste() functions accordingly. ''' - global Foundation, AppKit, gtk, qtpy, PyQt4, PyQt5 + global Foundation, AppKit, qtpy, 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' @@ -548,54 +532,45 @@ def determine_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 + import Foundation # check if pyobjc is installed except ImportError: return init_osx_pbcopy_clipboard() else: return init_osx_pyobjc_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 ( - os.environ.get("WAYLAND_DISPLAY") and - _executable_exists("wl-copy") - ): - return init_wl_clipboard() - if _executable_exists("xsel"): - return init_xsel_clipboard() + if os.getenv("WAYLAND_DISPLAY") and _executable_exists("wl-copy") and _executable_exists("wl-paste"): + return init_wl_clipboard() + + # `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. + elif os.getenv("DISPLAY"): if _executable_exists("xclip"): + # Note: 2024/06/18 Google Trends shows xclip as more popular than xsel. return init_xclip_clipboard() + if _executable_exists("xsel"): + return init_xsel_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. + # 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 PyQt4. - try: - import PyQt5 # check if PyQt5 is installed - except ImportError: - try: - import PyQt4 # check if PyQt4 is installed - except ImportError: - pass # We want to fail fast for all non-ImportError exceptions. - else: - return init_qt_clipboard() - else: - return init_qt_clipboard() - else: return init_qt_clipboard() + except ImportError: + pass + # If qtpy isn't installed, fall back on importing PyQt5 + try: + import PyQt5 # check if PyQt5 is installed + return init_qt_clipboard() + except ImportError: + pass return init_no_clipboard() @@ -607,7 +582,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 @@ -620,8 +594,7 @@ 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' + "qt": init_qt_clipboard, # TODO - split this into 'qtpy' and 'pyqt5' "xclip": init_xclip_clipboard, "xsel": init_xsel_clipboard, "wl-clipboard": init_wl_clipboard, @@ -644,7 +617,7 @@ def lazy_load_stub_copy(text): 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 + This could be a problem if it selects, say, the memory-heavy PyQt5 module but the user was just going to immediately call set_clipboard() to use a different clipboard mechanism. @@ -666,7 +639,7 @@ def lazy_load_stub_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 + This could be a problem if it selects, say, the memory-heavy PyQt5 module but the user was just going to immediately call set_clipboard() to use a different clipboard mechanism. @@ -692,44 +665,5 @@ def is_available(): -def waitForPaste(timeout=None): - """This function call blocks until a non-empty text string exists on the - clipboard. It returns this text. - - This function raises PyperclipTimeoutException if timeout was set to - a number of seconds that has elapsed without non-empty text being put on - the clipboard.""" - startTime = time.time() - while True: - clipboardText = paste() - if clipboardText != '': - return clipboardText - time.sleep(0.01) - - if timeout is not None and time.time() > startTime + timeout: - raise PyperclipTimeoutException('waitForPaste() timed out after ' + str(timeout) + ' seconds.') - - -def waitForNewPaste(timeout=None): - """This function call blocks until a new text string exists on the - clipboard that is different from the text that was there when the function - was first called. It returns this text. - - This function raises PyperclipTimeoutException if timeout was set to - a number of seconds that has elapsed without non-empty text being put on - the clipboard.""" - startTime = time.time() - originalText = paste() - while True: - currentText = paste() - if currentText != originalText: - return currentText - time.sleep(0.01) - - if timeout is not None and time.time() > startTime + timeout: - raise PyperclipTimeoutException('waitForNewPaste() timed out after ' + str(timeout) + ' seconds.') - - -__all__ = ['copy', 'paste', 'waitForPaste', 'waitForNewPaste', 'set_clipboard', 'determine_clipboard'] - +__all__ = ['copy', 'paste', 'set_clipboard', 'determine_clipboard']