summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorkotfu <kotfu@kotfu.net>2023-02-27 14:15:41 -0700
committerGitHub <noreply@github.com>2023-02-27 14:15:41 -0700
commitfbabd9bff4c66766f74d50e3c05eaf58e5a6f6de (patch)
tree009d3f7f540411cc9a8f7bce9e294e6f034a9874
parentee7599f9ac0dbb6ce3793f6b665ba1200d3ef9a3 (diff)
parent97b04689722be872e853017465adaaf1eb7cc591 (diff)
downloadcmd2-git-fbabd9bff4c66766f74d50e3c05eaf58e5a6f6de.tar.gz
Merge pull request #1258 from python-cmd2/clipboard
allow_clipboard and other clipboard improvements
-rw-r--r--CHANGELOG.md31
-rw-r--r--cmd2/clipboard.py24
-rw-r--r--cmd2/cmd2.py35
-rw-r--r--docs/api/cmd.rst7
-rw-r--r--docs/features/clipboard.rst8
-rwxr-xr-xsetup.py2
-rwxr-xr-xtests/test_cmd2.py65
7 files changed, 106 insertions, 66 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 57e04405..e65c6b40 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,9 @@
* `cmd2` 2.5 supports Python 3.7+ (removed support for Python 3.6)
* Enhancements
* Removed dependency on `attrs` and replaced with [dataclasses](https://docs.python.org/3/library/dataclasses.html)
+ * add `allow_clipboard` initialization parameter and attribute to disable ability to
+ add output to the operating system clipboard
+
## 2.4.3 (January 27, 2023)
* Bug Fixes
@@ -90,7 +93,7 @@
* Added `ap_completer_type` keyword arg to `Cmd2ArgumentParser.__init__()` which saves a call
to `set_ap_completer_type()`. This keyword will also work with `add_parser()` when creating subcommands
if the base command's parser is a `Cmd2ArgumentParser`.
- * New function `register_argparse_argument_parameter()` allows developers to specify custom
+ * New function `register_argparse_argument_parameter()` allows developers to specify custom
parameters to be passed to the argparse parser's `add_argument()` method. These parameters will
become accessible in the resulting argparse Action object when modifying `ArgparseCompleter` behavior.
* Using `SimpleTable` in the output for the following commands to improve appearance.
@@ -263,12 +266,12 @@
## 1.3.7 (August 27, 2020)
* Bug Fixes
- * Fixes an issue introduced in 1.3.0 with processing command strings containing terminator/separator
+ * Fixes an issue introduced in 1.3.0 with processing command strings containing terminator/separator
character(s) that are manually passed to a command that uses argparse.
## 1.3.6 (August 27, 2020)
* Breaking changes
- * The functions cmd2 adds to Namespaces (`get_statement()` and `get_handler()`) are now
+ * The functions cmd2 adds to Namespaces (`get_statement()` and `get_handler()`) are now
`Cmd2AttributeWrapper` objects named `cmd2_statement` and `cmd2_handler`. This makes it
easy to filter out which attributes in an `argparse.Namespace` were added by `cmd2`.
* Deprecations
@@ -294,7 +297,7 @@
* Breaking changes
* CommandSet command functions (do_, complete_, help_) will no longer have the cmd2 app
passed in as the first parameter after `self` since this is already a class member.
- * Renamed `install_command_set()` and `uninstall_command_set()` to `register_command_set()` and
+ * Renamed `install_command_set()` and `uninstall_command_set()` to `register_command_set()` and
`unregister_command_set()` for better name consistency.
* Bug Fixes
* Fixed help formatting bug in `Cmd2ArgumentParser` when `metavar` is a tuple
@@ -304,8 +307,8 @@
* Removed explicit type hints that fail due to a bug in 3.5.2 favoring comment-based hints instead
* When passing a ns_provider to an argparse command, will now attempt to resolve the correct
CommandSet instance for self. If not, it'll fall back and pass in the cmd2 app
-* Other
- * Added missing doc-string for new cmd2.Cmd __init__ parameters
+* Other
+ * Added missing doc-string for new cmd2.Cmd __init__ parameters
introduced by CommandSet enhancement
## 1.3.2 (August 10, 2020)
@@ -320,8 +323,8 @@
## 1.3.1 (August 6, 2020)
* Bug Fixes
* Fixed issue determining whether an argparse completer function required a reference to a containing
- CommandSet. Also resolves issues determining the correct CommandSet instance when calling the argparse
- argument completer function. Manifested as a TypeError when using `cmd2.Cmd.path_complete` as a completer
+ CommandSet. Also resolves issues determining the correct CommandSet instance when calling the argparse
+ argument completer function. Manifested as a TypeError when using `cmd2.Cmd.path_complete` as a completer
for an argparse-based command defined in a CommandSet
## 1.3.0 (August 4, 2020)
@@ -330,7 +333,7 @@
with your cmd2 application.
* Other
* Marked with_argparser_and_unknown_args pending deprecation and consolidated implementation into
- with_argparser
+ with_argparser
## 1.2.1 (July 14, 2020)
* Bug Fixes
@@ -338,7 +341,7 @@
## 1.2.0 (July 13, 2020)
* Bug Fixes
- * Fixed `typing` module compatibility issue with Python 3.5 prior to 3.5.4
+ * Fixed `typing` module compatibility issue with Python 3.5 prior to 3.5.4
* Enhancements
* Switched to getting version using `importlib.metadata` instead of using `pkg_resources`
* Improves `cmd2` application launch time on systems that have a lot of Python packages on `sys.path`
@@ -347,7 +350,7 @@
## 1.1.0 (June 6, 2020)
* Bug Fixes
* Fixed issue where subcommand usage text could contain a subcommand alias instead of the actual name
- * Fixed bug in `ArgparseCompleter` where `fill_width` could become negative if `token_width` was large
+ * Fixed bug in `ArgparseCompleter` where `fill_width` could become negative if `token_width` was large
relative to the terminal width.
* Enhancements
* Made `ipy` consistent with `py` in the following ways
@@ -371,7 +374,7 @@
after parsing fails, just return instead of raising an exception.
* Added explicit handling of `SystemExit`. If a command raises this exception, the command loop will be
gracefully stopped.
-
+
## 1.0.2 (April 06, 2020)
* Bug Fixes
* Ctrl-C now stops a running text script instead of just the current `run_script` command
@@ -394,9 +397,9 @@
* Bug Fixes
* Corrected issue where the actual new value was not always being printed in do_set. This occurred in cases where
the typed value differed from what the setter had converted it to.
- * Fixed bug where ANSI style sequences were not correctly handled in `utils.truncate_line()`.
+ * Fixed bug where ANSI style sequences were not correctly handled in `utils.truncate_line()`.
* Fixed bug where pyscripts could edit `cmd2.Cmd.py_locals` dictionary.
- * Fixed bug where cmd2 set `sys.path[0]` for a pyscript to cmd2's working directory instead of the
+ * Fixed bug where cmd2 set `sys.path[0]` for a pyscript to cmd2's working directory instead of the
script file's directory.
* Fixed bug where `sys.path` was not being restored after a pyscript ran.
* Enhancements
diff --git a/cmd2/clipboard.py b/cmd2/clipboard.py
index fa1c23a7..eda92bf6 100644
--- a/cmd2/clipboard.py
+++ b/cmd2/clipboard.py
@@ -2,37 +2,17 @@
"""
This module provides basic ability to copy from and paste to the clipboard/pastebuffer.
"""
-from typing import (
- cast,
-)
+import typing
import pyperclip # type: ignore[import]
-# noinspection PyProtectedMember
-
-# Can we access the clipboard? Should always be true on Windows and Mac, but only sometimes on Linux
-# noinspection PyBroadException
-try:
- # Try getting the contents of the clipboard
- _ = pyperclip.paste()
-
-# pyperclip raises at least the following types of exceptions. To be safe, just catch all Exceptions.
-# FileNotFoundError on Windows Subsystem for Linux (WSL) when Windows paths are removed from $PATH
-# ValueError for headless Linux systems without Gtk installed
-# AssertionError can be raised by paste_klipper().
-# PyperclipException for pyperclip-specific exceptions
-except Exception:
- can_clip = False
-else:
- can_clip = True
-
def get_paste_buffer() -> str:
"""Get the contents of the clipboard / paste buffer.
:return: contents of the clipboard
"""
- pb_str = cast(str, pyperclip.paste())
+ pb_str = typing.cast(str, pyperclip.paste())
return pb_str
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py
index 97cfb77d..bdabee9c 100644
--- a/cmd2/cmd2.py
+++ b/cmd2/cmd2.py
@@ -37,6 +37,7 @@ import os
import pydoc
import re
import sys
+import tempfile
import threading
from code import (
InteractiveConsole,
@@ -83,7 +84,6 @@ from .argparse_custom import (
CompletionItem,
)
from .clipboard import (
- can_clip,
get_paste_buffer,
write_to_paste_buffer,
)
@@ -234,6 +234,7 @@ class Cmd(cmd.Cmd):
shortcuts: Optional[Dict[str, str]] = None,
command_sets: Optional[Iterable[CommandSet]] = None,
auto_load_commands: bool = True,
+ allow_clipboard: bool = True,
) -> None:
"""An easy but powerful framework for writing line-oriented command
interpreters. Extends Python's cmd package.
@@ -281,6 +282,7 @@ class Cmd(cmd.Cmd):
that are currently loaded by Python and automatically
instantiate and register all commands. If False, CommandSets
must be manually installed with `register_command_set`.
+ :param allow_clipboard: If False, cmd2 will disable clipboard interactions
"""
# Check if py or ipy need to be disabled in this instance
if not include_py:
@@ -434,8 +436,8 @@ class Cmd(cmd.Cmd):
self.pager = 'less -RXF'
self.pager_chop = 'less -SRXF'
- # This boolean flag determines whether or not the cmd2 application can interact with the clipboard
- self._can_clip = can_clip
+ # This boolean flag stores whether cmd2 will allow clipboard related features
+ self.allow_clipboard = allow_clipboard
# This determines the value returned by cmdloop() when exiting the application
self.exit_code = 0
@@ -2722,13 +2724,8 @@ class Cmd(cmd.Cmd):
sys.stdout = self.stdout = new_stdout
elif statement.output:
- import tempfile
-
- if (not statement.output_to) and (not self._can_clip):
- raise RedirectionError("Cannot redirect to paste buffer; missing 'pyperclip' and/or pyperclip dependencies")
-
- # Redirecting to a file
- elif statement.output_to:
+ if statement.output_to:
+ # redirecting to a file
# statement.output can only contain REDIRECTION_APPEND or REDIRECTION_OUTPUT
mode = 'a' if statement.output == constants.REDIRECTION_APPEND else 'w'
try:
@@ -2740,14 +2737,26 @@ class Cmd(cmd.Cmd):
redir_saved_state.redirecting = True
sys.stdout = self.stdout = new_stdout
- # Redirecting to a paste buffer
else:
+ # Redirecting to a paste buffer
+ # we are going to direct output to a temporary file, then read it back in and
+ # put it in the paste buffer later
+ if not self.allow_clipboard:
+ raise RedirectionError("Clipboard access not allowed")
+
+ # attempt to get the paste buffer, this forces pyperclip to go figure
+ # out if it can actually interact with the paste buffer, and will throw exceptions
+ # if it's not gonna work. That way we throw the exception before we go
+ # run the command and queue up all the output. if this is going to fail,
+ # no point opening up the temporary file
+ current_paste_buffer = get_paste_buffer()
+ # create a temporary file to store output
new_stdout = cast(TextIO, tempfile.TemporaryFile(mode="w+"))
redir_saved_state.redirecting = True
sys.stdout = self.stdout = new_stdout
if statement.output == constants.REDIRECTION_APPEND:
- self.stdout.write(get_paste_buffer())
+ self.stdout.write(current_paste_buffer)
self.stdout.flush()
# These are updated regardless of whether the command redirected
@@ -4537,8 +4546,6 @@ class Cmd(cmd.Cmd):
self.last_result = True
return stop
elif args.edit:
- import tempfile
-
fd, fname = tempfile.mkstemp(suffix='.txt', text=True)
fobj: TextIO
with os.fdopen(fd, 'w') as fobj:
diff --git a/docs/api/cmd.rst b/docs/api/cmd.rst
index 6fdfbf27..f9bd07c6 100644
--- a/docs/api/cmd.rst
+++ b/docs/api/cmd.rst
@@ -65,3 +65,10 @@ cmd2.Cmd
The symbol name which :ref:`features/scripting:Python Scripts` run
using the :ref:`features/builtin_commands:run_pyscript` command can use
to reference the parent ``cmd2`` application.
+
+ .. attribute:: allow_clipboard
+
+ If ``True``, ``cmd2`` will allow output to be written to or appended to
+ the operating system pasteboard. If ``False``, this capability will not
+ be allowed. See :ref:`features/clipboard:Clipboard Integration` for more
+ information.
diff --git a/docs/features/clipboard.rst b/docs/features/clipboard.rst
index 73e206c2..a4b9cdf2 100644
--- a/docs/features/clipboard.rst
+++ b/docs/features/clipboard.rst
@@ -25,6 +25,14 @@ contents of the clipboard by ending the command with two greater than symbols:
Developers
----------
+You can control whether the above user features of adding output to the
+operating system clipboard are allowed for the user by setting the
+:attr:`~cmd2.Cmd.allow_clipboard` attribute. The default value is ``True``.
+Set it to ``False`` and the above functionality will generate an error
+message instead of adding the output to the clipboard.
+:attr:`~cmd2.Cmd.allow_clipboard` can be set upon initialization, and you can
+change it at any time from within your code.
+
If you would like your ``cmd2`` based application to be able to use the
clipboard in additional or alternative ways, you can use the following methods
(which work uniformly on Windows, macOS, and Linux).
diff --git a/setup.py b/setup.py
index b5724dd4..dae53695 100755
--- a/setup.py
+++ b/setup.py
@@ -66,6 +66,8 @@ EXTRAS_REQUIRE = {
'codecov',
'doc8',
'flake8',
+ 'black',
+ 'isort',
'invoke',
'mypy',
'nox',
diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py
index 5bcceb34..2270a907 100755
--- a/tests/test_cmd2.py
+++ b/tests/test_cmd2.py
@@ -171,9 +171,9 @@ now: True
out, err = run_cmd(base_app, 'set quiet')
expected = normalize(
"""
-Name Value Description
+Name Value Description
===================================================================================================
-quiet True Don't print nonessential feedback
+quiet True Don't print nonessential feedback
"""
)
assert out == expected
@@ -730,7 +730,21 @@ def test_pipe_to_shell_error(base_app):
assert "Pipe process exited with code" in err[0]
-@pytest.mark.skipif(not clipboard.can_clip, reason="Pyperclip could not find a copy/paste mechanism for your system")
+try:
+ # try getting the contents of the clipboard
+ _ = clipboard.get_paste_buffer()
+ # pyperclip raises at least the following types of exceptions
+ # FileNotFoundError on Windows Subsystem for Linux (WSL) when Windows paths are removed from $PATH
+ # ValueError for headless Linux systems without Gtk installed
+ # AssertionError can be raised by paste_klipper().
+ # PyperclipException for pyperclip-specific exceptions
+except Exception:
+ can_paste = False
+else:
+ can_paste = True
+
+
+@pytest.mark.skipif(not can_paste, reason="Pyperclip could not find a copy/paste mechanism for your system")
def test_send_to_paste_buffer(base_app):
# Test writing to the PasteBuffer/Clipboard
run_cmd(base_app, 'help >')
@@ -744,6 +758,38 @@ def test_send_to_paste_buffer(base_app):
assert len(appended_contents) > len(paste_contents)
+def test_get_paste_buffer_exception(base_app, mocker, capsys):
+ # Force get_paste_buffer to throw an exception
+ pastemock = mocker.patch('pyperclip.paste')
+ pastemock.side_effect = ValueError('foo')
+
+ # Redirect command output to the clipboard
+ base_app.onecmd_plus_hooks('help > ')
+
+ # Make sure we got the exception output
+ out, err = capsys.readouterr()
+ assert out == ''
+ # this just checks that cmd2 is surfacing whatever error gets raised by pyperclip.paste
+ assert 'ValueError' in err and 'foo' in err
+
+
+def test_allow_clipboard_initializer(base_app):
+ assert base_app.allow_clipboard == True
+ noclipcmd = cmd2.Cmd(allow_clipboard=False)
+ assert noclipcmd.allow_clipboard == False
+
+
+# if clipboard access is not allowed, cmd2 should check that first
+# before it tries to do anything with pyperclip, that's why we can
+# safely run this test without skipping it if pyperclip doesn't
+# work in the test environment, like we do for test_send_to_paste_buffer()
+def test_allow_clipboard(base_app):
+ base_app.allow_clipboard = False
+ out, err = run_cmd(base_app, 'help >')
+ assert not out
+ assert "Clipboard access not allowed" in err
+
+
def test_base_timing(base_app):
base_app.feedback_to_output = False
out, err = run_cmd(base_app, 'set timing True')
@@ -1566,19 +1612,6 @@ def test_multiline_input_line_to_statement(multiline_app):
assert statement.multiline_command == 'orate'
-def test_clipboard_failure(base_app, capsys):
- # Force cmd2 clipboard to be disabled
- base_app._can_clip = False
-
- # Redirect command output to the clipboard when a clipboard isn't present
- base_app.onecmd_plus_hooks('help > ')
-
- # Make sure we got the error output
- out, err = capsys.readouterr()
- assert out == ''
- assert 'Cannot redirect to paste buffer;' in err and 'pyperclip' in err
-
-
class CommandResultApp(cmd2.Cmd):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)