Skip to content

Commit

Permalink
ENH: use shutil.which() instead of external which(1) (#54937)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
mgorny authored Sep 8, 2023
1 parent 5c7abca commit 711fea0
Showing 1 changed file with 90 additions and 21 deletions.
111 changes: 90 additions & 21 deletions pandas/io/clipboard/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,12 @@
On Windows, no additional modules are needed.
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:
On Linux, install xclip, xsel, or wl-clipboard (for "wayland" sessions) via
package manager.
For example, in Debian:
sudo apt-get install xclip
sudo apt-get install xsel
sudo apt-get install wl-clipboard
Otherwise on Linux, you will need the PyQt5 modules installed.
Expand All @@ -28,20 +31,19 @@
Cygwin is currently not supported.
Security Note: This module runs programs with these names:
- which
- where
- pbcopy
- pbpaste
- xclip
- xsel
- wl-copy/wl-paste
- 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.
"""

__version__ = "1.7.0"
__version__ = "1.8.2"


import contextlib
Expand All @@ -55,7 +57,7 @@
)
import os
import platform
from shutil import which
from shutil import which as _executable_exists
import subprocess
import time
import warnings
Expand All @@ -74,25 +76,14 @@
EXCEPT_MSG = """
Pyperclip could not find a copy/paste mechanism for your system.
For more information, please visit
https://pyperclip.readthedocs.io/en/latest/#not-implemented-error
https://pyperclip.readthedocs.io/en/latest/index.html#not-implemented-error
"""

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(
[WHICH_CMD, name], stdout=subprocess.PIPE, stderr=subprocess.PIPE
)
== 0
)
class PyperclipTimeoutException(PyperclipException):
pass


def _stringifyText(text) -> str:
Expand Down Expand Up @@ -229,6 +220,32 @@ def paste_xsel(primary=False):
return copy_xsel, paste_xsel


def init_wl_clipboard():
PRIMARY_SELECTION = "-p"

def copy_wl(text, primary=False):
text = _stringifyText(text) # Converts non-str values to str.
args = ["wl-copy"]
if primary:
args.append(PRIMARY_SELECTION)
if not text:
args.append("--clear")
subprocess.check_call(args, close_fds=True)
else:
p = subprocess.Popen(args, stdin=subprocess.PIPE, close_fds=True)
p.communicate(input=text.encode(ENCODING))

def paste_wl(primary=False):
args = ["wl-paste", "-n"]
if primary:
args.append(PRIMARY_SELECTION)
p = subprocess.Popen(args, stdout=subprocess.PIPE, close_fds=True)
stdout, _stderr = p.communicate()
return stdout.decode(ENCODING)

return copy_wl, paste_wl


def init_klipper_clipboard():
def copy_klipper(text):
text = _stringifyText(text) # Converts non-str values to str.
Expand Down Expand Up @@ -534,7 +551,7 @@ def determine_clipboard():
return init_windows_clipboard()

if platform.system() == "Linux":
if which("wslconfig.exe"):
if _executable_exists("wslconfig.exe"):
return init_wsl_clipboard()

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

# Setup for the LINUX platform:
if HAS_DISPLAY:
if os.environ.get("WAYLAND_DISPLAY") and _executable_exists("wl-copy"):
return init_wl_clipboard()
if _executable_exists("xsel"):
return init_xsel_clipboard()
if _executable_exists("xclip"):
Expand Down Expand Up @@ -602,6 +621,7 @@ def set_clipboard(clipboard):
"qt": init_qt_clipboard, # TODO - split this into 'qtpy', 'pyqt4', and 'pyqt5'
"xclip": init_xclip_clipboard,
"xsel": init_xsel_clipboard,
"wl-clipboard": init_wl_clipboard,
"klipper": init_klipper_clipboard,
"windows": init_windows_clipboard,
"no": init_no_clipboard,
Expand Down Expand Up @@ -671,7 +691,56 @@ def is_available() -> bool:
copy, paste = lazy_load_stub_copy, lazy_load_stub_paste


__all__ = ["copy", "paste", "set_clipboard", "determine_clipboard"]
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",
]

# pandas aliases
clipboard_get = paste
Expand Down

0 comments on commit 711fea0

Please sign in to comment.