Skip to content

Commit

Permalink
Bumping version to 1.9.0.
Browse files Browse the repository at this point in the history
Automatically cast the copy() argument to a string for all data types.

_py3_executable_exists and _py2_executable_exists had swapped names; fixed.

Pyperclip now "stringifies" all data types by passing it to str() (or globals()['__builtins__'].unicode on Python 2), so passing [1, 2, 3] would put '[1, 2, 3]' on the clipboard.

shutil.which() replaces the custom code (except in 2.7 and below which doesn't have shutil.which()).

Remove waitForPaste() and waitForNewPaste() functions, these aren't something the core library should have.

Reordered so that xclip is chosen before xsel since xclip is more popular.

Removed "where" command for Windows because we don't need it on Windows.
  • Loading branch information
asweigart committed Jun 18, 2024
1 parent 078b4b6 commit 5aef21c
Showing 1 changed file with 46 additions and 92 deletions.
138 changes: 46 additions & 92 deletions src/pyperclip/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@
Security Note: This module runs programs with these names:
- which
- where
- pbcopy
- pbpaste
- xclip
Expand All @@ -46,7 +45,7 @@
Pyperclip into running them with whatever permissions the Python process has.
"""
__version__ = '1.8.3'
__version__ = '1.9.0'

import contextlib
import ctypes
Expand All @@ -58,37 +57,32 @@
import warnings

from ctypes import c_size_t, sizeof, c_wchar_p, get_errno, c_wchar
from typing import Union, Optional


# `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)
_IS_RUNNING_PYTHON_2 = sys.version_info[0] == 2 # type: bool

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 """
# 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 # type: ignore
else:
_PYTHON_STR_TYPE = str

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'
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.call(['which', name],
stdout=subprocess.PIPE, stderr=subprocess.PIPE) == 0


_executable_exists = _py2_executable_exists

# Exceptions
class PyperclipException(RuntimeError):
Expand All @@ -102,20 +96,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))
Expand All @@ -132,7 +116,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()
Expand All @@ -154,7 +138,7 @@ def init_gtk_clipboard():

def copy_gtk(text):
global cb
text = _stringifyText(text) # Converts non-str values to str.
text = _PYTHON_STR_TYPE(text) # Converts non-str values to str.
cb = gtk.Clipboard()
cb.set_text(text)
cb.store()
Expand Down Expand Up @@ -188,13 +172,13 @@ def init_qt_clipboard():
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

Expand All @@ -204,7 +188,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
Expand Down Expand Up @@ -232,7 +216,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
Expand All @@ -256,7 +240,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)
Expand All @@ -281,7 +265,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)],
Expand Down Expand Up @@ -310,7 +294,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:
Expand All @@ -333,9 +317,12 @@ 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:
def __nonzero__(self):
return False
else:
Expand Down Expand Up @@ -460,7 +447,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
Expand Down Expand Up @@ -505,7 +492,7 @@ def 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))
Expand Down Expand Up @@ -560,7 +547,11 @@ def determine_clipboard():
return init_osx_pyobjc_clipboard()

# Setup for the LINUX platform:
if HAS_DISPLAY:

# `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.
if os.getenv("DISPLAY"):
try:
import gtk # check if gtk is installed
except ImportError:
Expand All @@ -569,14 +560,15 @@ def determine_clipboard():
return init_gtk_clipboard()

if (
os.environ.get("WAYLAND_DISPLAY") and
os.getenv("WAYLAND_DISPLAY") and
_executable_exists("wl-copy")
):
return init_wl_clipboard()
if _executable_exists("xsel"):
return init_xsel_clipboard()
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()

Expand Down Expand Up @@ -696,44 +688,6 @@ 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']


0 comments on commit 5aef21c

Please sign in to comment.