Skip to content

Commit 604adde

Browse files
committed
[Clipboard] fixes + only activate PyperClip if Cmd is set with allow_clipboard = True argument
1 parent 60f6f5f commit 604adde

2 files changed

Lines changed: 25 additions & 31 deletions

File tree

cmd2/cmd2.py

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -395,7 +395,7 @@ def __init__(
395395
line arguments as either commands to be run. This should be
396396
set to ``False`` if your application parses its own command line
397397
arguments.
398-
:param allow_clipboard: If ``False``, cmd2 will disable clipboard interactions
398+
:param allow_clipboard: If ``False``, cmd2 will disable system clipboard interactions
399399
:param allow_redirection: If ``False``, prevent output redirection and piping to shell
400400
commands. This parameter prevents redirection and piping, but
401401
does not alter parsing behavior. A user can still type
@@ -541,6 +541,9 @@ def __init__(
541541
self._persistent_history_length = persistent_history_length
542542
self._initialize_history(persistent_history_file)
543543

544+
# This boolean flag stores whether cmd2 will allow clipboard related features
545+
self.allow_clipboard = allow_clipboard
546+
544547
# Create the main PromptSession
545548
self.main_session = self._create_main_session(
546549
auto_suggest=auto_suggest,
@@ -652,9 +655,6 @@ def __init__(
652655
self.pager = "less -RXF"
653656
self.pager_chop = "less -SRXF"
654657

655-
# This boolean flag stores whether cmd2 will allow clipboard related features
656-
self.allow_clipboard = allow_clipboard
657-
658658
# This determines the value returned by cmdloop() when exiting the application
659659
self.exit_code = 0
660660

@@ -795,17 +795,18 @@ def _(event: Any) -> None: # pragma: no cover
795795
"style": DynamicStyle(get_pt_theme),
796796
}
797797

798-
# Only enable PyperclipClipboard if the system clipboard is accessible to Pyperclip.
799-
try:
800-
cb = PyperclipClipboard()
801-
cb.get_data() # Check if the system clipboard is accessible to Pyperclip
802-
except Exception: # noqa: BLE001, S110
803-
# Prevent prompt_toolkit from crashing in headless environments and fallback
804-
# on prompt toolkit's default clipboard (InMemoryClipboard) by not providing
805-
# any argument for 'clipboard' in kwargs
806-
pass
807-
else:
808-
kwargs["clipboard"] = cb
798+
if self.allow_clipboard:
799+
# Only enable PyperclipClipboard if the system clipboard is accessible to Pyperclip.
800+
try:
801+
cb = PyperclipClipboard()
802+
cb.get_data() # Check if the system clipboard is accessible to Pyperclip
803+
except Exception: # noqa: BLE001, S110
804+
# Prevent prompt_toolkit from crashing in headless environments and fallback
805+
# on prompt toolkit's default clipboard (InMemoryClipboard) by not providing
806+
# any argument for 'clipboard' in kwargs
807+
pass
808+
else:
809+
kwargs["clipboard"] = cb
809810

810811
if self.stdin.isatty() and self.stdout.isatty():
811812
try:
@@ -3036,7 +3037,7 @@ def onecmd_plus_hooks(
30363037
pass
30373038
except Cmd2ShlexError as ex:
30383039
self.perror(f"Invalid syntax: {ex}")
3039-
except RedirectionError as ex:
3040+
except (ClipboardError, RedirectionError) as ex:
30403041
self.perror(ex)
30413042
except KeyboardInterrupt:
30423043
if raise_keyboard_interrupt and not stop:
@@ -3266,6 +3267,7 @@ def _redirect_output(self, statement: Statement) -> utils.RedirectionSavedState:
32663267
:param statement: a parsed statement from the user
32673268
:return: A bool telling if an error occurred and a utils.RedirectionSavedState object
32683269
:raises RedirectionError: if an error occurs trying to pipe or redirect
3270+
:raises ClipboardError: if an error occurs trying to access clipboard data
32693271
"""
32703272
import subprocess
32713273

@@ -3359,7 +3361,7 @@ def _redirect_output(self, statement: Statement) -> utils.RedirectionSavedState:
33593361
try:
33603362
current_paste_buffer = self.clipboard.get_data().text
33613363
except Exception as ex:
3362-
raise ClipboardError(f"Failed to access clipboard data: {ex}") from ex
3364+
raise ClipboardError("Failed to access clipboard data") from ex
33633365

33643366
# create a temporary file to store output
33653367
new_stdout = cast(TextIO, tempfile.TemporaryFile(mode="w+")) # noqa: SIM115
@@ -3380,6 +3382,7 @@ def _restore_output(self, statement: Statement, saved_redir_state: utils.Redirec
33803382
33813383
:param statement: Statement object which contains the parsed input from the user
33823384
:param saved_redir_state: contains information needed to restore state data
3385+
:raises ClipboardError: if an error occurs while trying to set the clipboard data
33833386
"""
33843387
if saved_redir_state.redirecting:
33853388
try:
@@ -3394,7 +3397,7 @@ def _restore_output(self, statement: Statement, saved_redir_state: utils.Redirec
33943397
try:
33953398
self.clipboard.set_text(self.stdout.read())
33963399
except Exception as ex:
3397-
raise ClipboardError(f"Failed to set clipboard data: {ex}") from ex
3400+
raise ClipboardError("Failed to set clipboard data") from ex
33983401
finally:
33993402
with contextlib.suppress(BrokenPipeError):
34003403
# Close the file or pipe that stdout was redirected to

tests/test_cmd2.py

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -919,10 +919,7 @@ def test_init_with_no_clipboard_allowed() -> None:
919919
app = cmd2.Cmd(allow_clipboard=False)
920920

921921
# Check for the clipboard type
922-
if pyperclip_can_paste:
923-
assert isinstance(app.clipboard, PyperclipClipboard)
924-
else:
925-
assert isinstance(app.clipboard, InMemoryClipboard)
922+
assert isinstance(app.clipboard, InMemoryClipboard)
926923

927924

928925
def test_init_with_clipboard_allowed() -> None:
@@ -960,9 +957,7 @@ def test_get_paste_copy_exception_overwrite(redirection_app, mocker, capsys) ->
960957
out, err = capsys.readouterr()
961958

962959
# this just checks that cmd2 is surfacing whatever error gets raised by pyperclip.copy
963-
assert "ClipboardError" in err
964-
assert "Failed to set clipboard data" in err
965-
assert "copy fail" in err
960+
assert err == "Failed to set clipboard data\n"
966961

967962
# check stdout
968963
assert out == "print\n"
@@ -983,9 +978,7 @@ def test_get_paste_copy_exception_append(redirection_app, mocker, capsys) -> Non
983978
out, err = capsys.readouterr()
984979

985980
# this just checks that cmd2 is surfacing whatever error gets raised by pyperclip.copy
986-
assert "ClipboardError" in err
987-
assert "Failed to set clipboard data" in err
988-
assert "copy fail" in err
981+
assert err == "Failed to set clipboard data\n"
989982

990983
# check stdout
991984
assert out == "print\n"
@@ -1028,9 +1021,7 @@ def test_get_paste_buffer_exception_append(redirection_app, mocker, capsys) -> N
10281021
out, err = capsys.readouterr()
10291022

10301023
# this just checks that cmd2 is surfacing whatever error gets raised by pyperclip.paste
1031-
assert "ClipboardError" in err
1032-
assert "Failed to access clipboard data" in err
1033-
assert "paste fail" in err
1024+
assert err == "Failed to access clipboard data\n"
10341025

10351026
# check stdout
10361027
assert out == ""

0 commit comments

Comments
 (0)