Skip to content

Commit c875c58

Browse files
committed
Add allow_clipboard for #1225
1 parent 031832a commit c875c58

File tree

3 files changed

+67
-51
lines changed

3 files changed

+67
-51
lines changed

cmd2/clipboard.py

Lines changed: 2 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,37 +2,16 @@
22
"""
33
This module provides basic ability to copy from and paste to the clipboard/pastebuffer.
44
"""
5-
from typing import (
6-
cast,
7-
)
8-
5+
import typing
96
import pyperclip # type: ignore[import]
107

11-
# noinspection PyProtectedMember
12-
13-
# Can we access the clipboard? Should always be true on Windows and Mac, but only sometimes on Linux
14-
# noinspection PyBroadException
15-
try:
16-
# Try getting the contents of the clipboard
17-
_ = pyperclip.paste()
18-
19-
# pyperclip raises at least the following types of exceptions. To be safe, just catch all Exceptions.
20-
# FileNotFoundError on Windows Subsystem for Linux (WSL) when Windows paths are removed from $PATH
21-
# ValueError for headless Linux systems without Gtk installed
22-
# AssertionError can be raised by paste_klipper().
23-
# PyperclipException for pyperclip-specific exceptions
24-
except Exception:
25-
can_clip = False
26-
else:
27-
can_clip = True
28-
298

309
def get_paste_buffer() -> str:
3110
"""Get the contents of the clipboard / paste buffer.
3211
3312
:return: contents of the clipboard
3413
"""
35-
pb_str = cast(str, pyperclip.paste())
14+
pb_str = typing.cast(str, pyperclip.paste())
3615
return pb_str
3716

3817

cmd2/cmd2.py

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@
3737
import pydoc
3838
import re
3939
import sys
40+
import tempfile
41+
import typing
4042
import threading
4143
from code import (
4244
InteractiveConsole,
@@ -83,7 +85,6 @@
8385
CompletionItem,
8486
)
8587
from .clipboard import (
86-
can_clip,
8788
get_paste_buffer,
8889
write_to_paste_buffer,
8990
)
@@ -236,6 +237,7 @@ def __init__(
236237
shortcuts: Optional[Dict[str, str]] = None,
237238
command_sets: Optional[Iterable[CommandSet]] = None,
238239
auto_load_commands: bool = True,
240+
allow_clipboard: bool = True,
239241
) -> None:
240242
"""An easy but powerful framework for writing line-oriented command
241243
interpreters. Extends Python's cmd package.
@@ -283,6 +285,7 @@ def __init__(
283285
that are currently loaded by Python and automatically
284286
instantiate and register all commands. If False, CommandSets
285287
must be manually installed with `register_command_set`.
288+
:param allow_clipboard: If False, cmd2 will disable clipboard interactions
286289
"""
287290
# Check if py or ipy need to be disabled in this instance
288291
if not include_py:
@@ -436,8 +439,8 @@ def __init__(
436439
self.pager = 'less -RXF'
437440
self.pager_chop = 'less -SRXF'
438441

439-
# This boolean flag determines whether or not the cmd2 application can interact with the clipboard
440-
self._can_clip = can_clip
442+
# This boolean flag stores whether cmd2 will allow clipboard related features
443+
self.allow_clipboard = allow_clipboard
441444

442445
# This determines the value returned by cmdloop() when exiting the application
443446
self.exit_code = 0
@@ -2734,13 +2737,9 @@ def _redirect_output(self, statement: Statement) -> utils.RedirectionSavedState:
27342737
sys.stdout = self.stdout = new_stdout
27352738

27362739
elif statement.output:
2737-
import tempfile
27382740

2739-
if (not statement.output_to) and (not self._can_clip):
2740-
raise RedirectionError("Cannot redirect to paste buffer; missing 'pyperclip' and/or pyperclip dependencies")
2741-
2742-
# Redirecting to a file
2743-
elif statement.output_to:
2741+
if statement.output_to:
2742+
# redirecting to a file
27442743
# statement.output can only contain REDIRECTION_APPEND or REDIRECTION_OUTPUT
27452744
mode = 'a' if statement.output == constants.REDIRECTION_APPEND else 'w'
27462745
try:
@@ -2752,14 +2751,26 @@ def _redirect_output(self, statement: Statement) -> utils.RedirectionSavedState:
27522751
redir_saved_state.redirecting = True
27532752
sys.stdout = self.stdout = new_stdout
27542753

2755-
# Redirecting to a paste buffer
27562754
else:
2755+
# Redirecting to a paste buffer
2756+
# we are going to direct output to a temporary file, then read it back in and
2757+
# put it in the paste buffer later
2758+
if not self.allow_clipboard:
2759+
raise RedirectionError("Clipboard access not allowed")
2760+
2761+
# attempt to get the paste buffer, this forces pyperclip to go figure
2762+
# out if it can actually interact with the paste buffer, and will throw exceptions
2763+
# if it's not gonna work. That way we throw the exception before we go
2764+
# run the command and queue up all the output. if this is going to fail,
2765+
# no point opening up the temporary file
2766+
current_paste_buffer = get_paste_buffer()
2767+
# create a temporary file to store output
27572768
new_stdout = cast(TextIO, tempfile.TemporaryFile(mode="w+"))
27582769
redir_saved_state.redirecting = True
27592770
sys.stdout = self.stdout = new_stdout
27602771

27612772
if statement.output == constants.REDIRECTION_APPEND:
2762-
self.stdout.write(get_paste_buffer())
2773+
self.stdout.write(current_paste_buffer)
27632774
self.stdout.flush()
27642775

27652776
# These are updated regardless of whether the command redirected
@@ -4551,8 +4562,6 @@ def do_history(self, args: argparse.Namespace) -> Optional[bool]:
45514562
self.last_result = True
45524563
return stop
45534564
elif args.edit:
4554-
import tempfile
4555-
45564565
fd, fname = tempfile.mkstemp(suffix='.txt', text=True)
45574566
fobj: TextIO
45584567
with os.fdopen(fd, 'w') as fobj:

tests/test_cmd2.py

Lines changed: 43 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -729,8 +729,20 @@ def test_pipe_to_shell_error(base_app):
729729
assert not out
730730
assert "Pipe process exited with code" in err[0]
731731

732-
733-
@pytest.mark.skipif(not clipboard.can_clip, reason="Pyperclip could not find a copy/paste mechanism for your system")
732+
try:
733+
# try getting the contents of the clipboard
734+
_ = clipboard.get_paste_buffer()
735+
# pyperclip raises at least the following types of exceptions
736+
# FileNotFoundError on Windows Subsystem for Linux (WSL) when Windows paths are removed from $PATH
737+
# ValueError for headless Linux systems without Gtk installed
738+
# AssertionError can be raised by paste_klipper().
739+
# PyperclipException for pyperclip-specific exceptions
740+
except Exception:
741+
can_paste = False
742+
else:
743+
can_paste = True
744+
745+
@pytest.mark.skipif(can_paste, reason="Pyperclip could not find a copy/paste mechanism for your system")
734746
def test_send_to_paste_buffer(base_app):
735747
# Test writing to the PasteBuffer/Clipboard
736748
run_cmd(base_app, 'help >')
@@ -743,6 +755,35 @@ def test_send_to_paste_buffer(base_app):
743755
assert appended_contents.startswith(paste_contents)
744756
assert len(appended_contents) > len(paste_contents)
745757

758+
def test_get_paste_buffer_exception(base_app, mocker, capsys):
759+
# Force get_paste_buffer to throw an exception
760+
pastemock = mocker.patch('pyperclip.paste')
761+
pastemock.side_effect = ValueError('foo')
762+
763+
# Redirect command output to the clipboard
764+
base_app.onecmd_plus_hooks('help > ')
765+
766+
# Make sure we got the exception output
767+
out, err = capsys.readouterr()
768+
assert out == ''
769+
# this just checks that cmd2 is surfacing whatever error gets raised by pyperclip.paste
770+
assert 'ValueError' in err and 'foo' in err
771+
772+
def test_allow_clipboard_initializer(base_app):
773+
assert base_app.allow_clipboard == True
774+
noclipcmd = cmd2.Cmd(allow_clipboard=False)
775+
assert noclipcmd.allow_clipboard == False
776+
777+
# if clipboard access is not allowed, cmd2 should check that first
778+
# before it tries to do anything with pyperclip, that's why we can
779+
# safely run this test without skipping it if pyperclip doesn't
780+
# work in the test environment, like we do for test_send_to_paste_buffer()
781+
def test_allow_clipboard(base_app):
782+
base_app.allow_clipboard = False
783+
out, err = run_cmd(base_app, 'help >')
784+
assert not out
785+
assert "Clipboard access not allowed" in err
786+
746787

747788
def test_base_timing(base_app):
748789
base_app.feedback_to_output = False
@@ -1566,19 +1607,6 @@ def test_multiline_input_line_to_statement(multiline_app):
15661607
assert statement.multiline_command == 'orate'
15671608

15681609

1569-
def test_clipboard_failure(base_app, capsys):
1570-
# Force cmd2 clipboard to be disabled
1571-
base_app._can_clip = False
1572-
1573-
# Redirect command output to the clipboard when a clipboard isn't present
1574-
base_app.onecmd_plus_hooks('help > ')
1575-
1576-
# Make sure we got the error output
1577-
out, err = capsys.readouterr()
1578-
assert out == ''
1579-
assert 'Cannot redirect to paste buffer;' in err and 'pyperclip' in err
1580-
1581-
15821610
class CommandResultApp(cmd2.Cmd):
15831611
def __init__(self, *args, **kwargs):
15841612
super().__init__(*args, **kwargs)

0 commit comments

Comments
 (0)