Skip to content

Commit 711fea0

Browse files
authored
ENH: use shutil.which() instead of external which(1) (#54937)
* ENH: update bundled pyperclip with changes from 1.8.2 release Copy the changes from upstream 1.8.2 to the bundled copy of pyperclip. The code was reformatted using black and verified using ruff. The existing modifications from pandas were preserved. * ENH: Remove Python 2 compatibility from imported pyperclip code Remove the fallback to which/where that is only necessary for Python 2 that does not feature shutil.which(). Also collapse the imports to avoid importing shutil.which() twice. It is now only imported as `_executable_exists()` to minimize the changes to the original code. * BUG: Fix pylint failure (redundant `pass`) in clipboard
1 parent 5c7abca commit 711fea0

File tree

1 file changed

+90
-21
lines changed

1 file changed

+90
-21
lines changed

pandas/io/clipboard/__init__.py

+90-21
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,12 @@
1717
On Windows, no additional modules are needed.
1818
On Mac, the pyobjc module is used, falling back to the pbcopy and pbpaste cli
1919
commands. (These commands should come with OS X.).
20-
On Linux, install xclip or xsel via package manager. For example, in Debian:
20+
On Linux, install xclip, xsel, or wl-clipboard (for "wayland" sessions) via
21+
package manager.
22+
For example, in Debian:
2123
sudo apt-get install xclip
2224
sudo apt-get install xsel
25+
sudo apt-get install wl-clipboard
2326
2427
Otherwise on Linux, you will need the PyQt5 modules installed.
2528
@@ -28,20 +31,19 @@
2831
Cygwin is currently not supported.
2932
3033
Security Note: This module runs programs with these names:
31-
- which
32-
- where
3334
- pbcopy
3435
- pbpaste
3536
- xclip
3637
- xsel
38+
- wl-copy/wl-paste
3739
- klipper
3840
- qdbus
3941
A malicious user could rename or add programs with these names, tricking
4042
Pyperclip into running them with whatever permissions the Python process has.
4143
4244
"""
4345

44-
__version__ = "1.7.0"
46+
__version__ = "1.8.2"
4547

4648

4749
import contextlib
@@ -55,7 +57,7 @@
5557
)
5658
import os
5759
import platform
58-
from shutil import which
60+
from shutil import which as _executable_exists
5961
import subprocess
6062
import time
6163
import warnings
@@ -74,25 +76,14 @@
7476
EXCEPT_MSG = """
7577
Pyperclip could not find a copy/paste mechanism for your system.
7678
For more information, please visit
77-
https://pyperclip.readthedocs.io/en/latest/#not-implemented-error
79+
https://pyperclip.readthedocs.io/en/latest/index.html#not-implemented-error
7880
"""
7981

8082
ENCODING = "utf-8"
8183

82-
# The "which" unix command finds where a command is.
83-
if platform.system() == "Windows":
84-
WHICH_CMD = "where"
85-
else:
86-
WHICH_CMD = "which"
8784

88-
89-
def _executable_exists(name):
90-
return (
91-
subprocess.call(
92-
[WHICH_CMD, name], stdout=subprocess.PIPE, stderr=subprocess.PIPE
93-
)
94-
== 0
95-
)
85+
class PyperclipTimeoutException(PyperclipException):
86+
pass
9687

9788

9889
def _stringifyText(text) -> str:
@@ -229,6 +220,32 @@ def paste_xsel(primary=False):
229220
return copy_xsel, paste_xsel
230221

231222

223+
def init_wl_clipboard():
224+
PRIMARY_SELECTION = "-p"
225+
226+
def copy_wl(text, primary=False):
227+
text = _stringifyText(text) # Converts non-str values to str.
228+
args = ["wl-copy"]
229+
if primary:
230+
args.append(PRIMARY_SELECTION)
231+
if not text:
232+
args.append("--clear")
233+
subprocess.check_call(args, close_fds=True)
234+
else:
235+
p = subprocess.Popen(args, stdin=subprocess.PIPE, close_fds=True)
236+
p.communicate(input=text.encode(ENCODING))
237+
238+
def paste_wl(primary=False):
239+
args = ["wl-paste", "-n"]
240+
if primary:
241+
args.append(PRIMARY_SELECTION)
242+
p = subprocess.Popen(args, stdout=subprocess.PIPE, close_fds=True)
243+
stdout, _stderr = p.communicate()
244+
return stdout.decode(ENCODING)
245+
246+
return copy_wl, paste_wl
247+
248+
232249
def init_klipper_clipboard():
233250
def copy_klipper(text):
234251
text = _stringifyText(text) # Converts non-str values to str.
@@ -534,7 +551,7 @@ def determine_clipboard():
534551
return init_windows_clipboard()
535552

536553
if platform.system() == "Linux":
537-
if which("wslconfig.exe"):
554+
if _executable_exists("wslconfig.exe"):
538555
return init_wsl_clipboard()
539556

540557
# Setup for the macOS platform:
@@ -549,6 +566,8 @@ def determine_clipboard():
549566

550567
# Setup for the LINUX platform:
551568
if HAS_DISPLAY:
569+
if os.environ.get("WAYLAND_DISPLAY") and _executable_exists("wl-copy"):
570+
return init_wl_clipboard()
552571
if _executable_exists("xsel"):
553572
return init_xsel_clipboard()
554573
if _executable_exists("xclip"):
@@ -602,6 +621,7 @@ def set_clipboard(clipboard):
602621
"qt": init_qt_clipboard, # TODO - split this into 'qtpy', 'pyqt4', and 'pyqt5'
603622
"xclip": init_xclip_clipboard,
604623
"xsel": init_xsel_clipboard,
624+
"wl-clipboard": init_wl_clipboard,
605625
"klipper": init_klipper_clipboard,
606626
"windows": init_windows_clipboard,
607627
"no": init_no_clipboard,
@@ -671,7 +691,56 @@ def is_available() -> bool:
671691
copy, paste = lazy_load_stub_copy, lazy_load_stub_paste
672692

673693

674-
__all__ = ["copy", "paste", "set_clipboard", "determine_clipboard"]
694+
def waitForPaste(timeout=None):
695+
"""This function call blocks until a non-empty text string exists on the
696+
clipboard. It returns this text.
697+
698+
This function raises PyperclipTimeoutException if timeout was set to
699+
a number of seconds that has elapsed without non-empty text being put on
700+
the clipboard."""
701+
startTime = time.time()
702+
while True:
703+
clipboardText = paste()
704+
if clipboardText != "":
705+
return clipboardText
706+
time.sleep(0.01)
707+
708+
if timeout is not None and time.time() > startTime + timeout:
709+
raise PyperclipTimeoutException(
710+
"waitForPaste() timed out after " + str(timeout) + " seconds."
711+
)
712+
713+
714+
def waitForNewPaste(timeout=None):
715+
"""This function call blocks until a new text string exists on the
716+
clipboard that is different from the text that was there when the function
717+
was first called. It returns this text.
718+
719+
This function raises PyperclipTimeoutException if timeout was set to
720+
a number of seconds that has elapsed without non-empty text being put on
721+
the clipboard."""
722+
startTime = time.time()
723+
originalText = paste()
724+
while True:
725+
currentText = paste()
726+
if currentText != originalText:
727+
return currentText
728+
time.sleep(0.01)
729+
730+
if timeout is not None and time.time() > startTime + timeout:
731+
raise PyperclipTimeoutException(
732+
"waitForNewPaste() timed out after " + str(timeout) + " seconds."
733+
)
734+
735+
736+
__all__ = [
737+
"copy",
738+
"paste",
739+
"waitForPaste",
740+
"waitForNewPaste",
741+
"set_clipboard",
742+
"determine_clipboard",
743+
]
675744

676745
# pandas aliases
677746
clipboard_get = paste

0 commit comments

Comments
 (0)