Skip to content

Commit

Permalink
Merge branch 'master' into remove-deprecated-libraries
Browse files Browse the repository at this point in the history
  • Loading branch information
asweigart authored Jun 18, 2024
2 parents 7edce5f + eb92ba6 commit 2856acf
Show file tree
Hide file tree
Showing 7 changed files with 151 additions and 297 deletions.
4 changes: 3 additions & 1 deletion AUTHORS.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,21 @@ Cees Timmerman https://github.com/CTimmerman
Chris Clark
Christopher Lambert https://github.com/XN137
Chris Woerz https://github.com/erendrake
Daniel Shimon https://github.com/daniel-shimon
Edd Barrett https://github.com/vext01
Eugene Yang https://github.com/eugene-yang
Felix Yan https://github.com/felixonmars
fthoma https://github.com/fthoma
Greg Witt https://github.com/GoodGuyGregory
hinlader https://github.com/hinlader
Hugo https://github.com/hugovk
Hugo van Kemenade https://github.com/hugovk
Hynek Cernoch https://github.com/hynekcer
Jason R. Coombs https://github.com/jaraco
Jon Crall https://github.com/Erotemic
Jonathan Slenders https://github.com/jonathanslenders
JustAShoeMaker https://github.com/JustAShoeMaker
Marcelo Glezer https://github.com/gato
masajxxx https://github.com/masajxxx
Maximilian Hils https://github.com/mhils
Michał Górny https://github.com/mgorny
Nikolaos-Digenis Karagiannis https://github.com/Digenis
Expand Down
3 changes: 1 addition & 2 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ pytest = "*"
pyperclip = {editable = true, path = "."}

[dev-packages]
tox = "*"
detox = "*"
mypy = "*"

[requires]
python_version = "3.9"
268 changes: 78 additions & 190 deletions Pipfile.lock

Large diffs are not rendered by default.

6 changes: 4 additions & 2 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,10 @@ You may get an error message that says: "Pyperclip could not find a copy/paste m

In order to work equally well on Windows, Mac, and Linux, Pyperclip uses various mechanisms to do this. Currently, this error should only appear on Linux (not Windows or Mac). You can fix this by installing one of the copy/paste mechanisms:

- ``sudo apt-get install xsel`` to install the xsel utility.
- ``sudo apt-get install xclip`` to install the xclip utility.
- ``sudo apt-get install xsel`` to install the ``xsel`` utility (for X11).
- ``sudo apt-get install xclip`` to install the ``xclip`` utility (for X11).
- ``sudo apt-get install wl-clipboard`` to install the ``wl-clipboard`` utility (for Wayland).
- ``pip install gtk`` to install the gtk Python module.
- ``pip install PyQt5`` to install the PyQt5 Python module.

Pyperclip won't work on mobile operating systems such as Android or iOS, nor in browser-based interactive shells such as `replit.com <https://replit.com>`_, `pythontutor.com <http://pythontutor.com>`_, or `pythonanywhere.com <https://pythonanywhere.com>`_.
7 changes: 2 additions & 5 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,17 +34,14 @@
'Operating System :: OS Independent',
'Programming Language :: Python',
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.6',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.1',
'Programming Language :: Python :: 3.2',
'Programming Language :: Python :: 3.3',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
],
)

158 changes: 62 additions & 96 deletions src/pyperclip/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@
Security Note: This module runs programs with these names:
- which
- where
- pbcopy
- pbpaste
- xclip
Expand All @@ -45,8 +44,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
Expand All @@ -57,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 PyQt5` sys.exit()s if DISPLAY is not in the environment.
# Thus, we need to detect the presence of $DISPLAY manually
# and not load PyQt5 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 @@ -101,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 @@ -131,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 Down Expand Up @@ -162,13 +147,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 @@ -178,7 +163,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 @@ -206,7 +191,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 @@ -230,7 +215,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 @@ -255,7 +240,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 @@ -284,7 +269,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 @@ -307,9 +292,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 @@ -434,7 +422,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 @@ -478,21 +466,32 @@ 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))
p.communicate(input=text.encode('utf-16le'))

def paste_wsl():
ps_script = '[Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes((Get-Clipboard -Raw)))'

# '-noprofile' speeds up load time
p = subprocess.Popen(['powershell.exe', '-noprofile', '-command', 'Get-Clipboard'],
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

Expand Down Expand Up @@ -534,16 +533,21 @@ 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"):
if (
os.environ.get("WAYLAND_DISPLAY") and
_executable_exists("wl-copy")
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 @@ -657,44 +661,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']


2 changes: 1 addition & 1 deletion tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
# and then run "tox" from this directory.

[tox]
envlist = py27, py34, py35, py36, py37, py38, py39
envlist = py27, py35, py36, py37, py38, py39, py310

[testenv]
deps =
Expand Down

0 comments on commit 2856acf

Please sign in to comment.