Skip to content

Commit ed54110

Browse files
authored
Merge pull request #155 from python-cmd2/pyperclip
Added 3rd-party dependency on pyperclip for clipboard copy/paste functionality
2 parents 309c3ec + 613a1b7 commit ed54110

8 files changed

Lines changed: 42 additions & 149 deletions

File tree

CHANGES.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ News
1717
* Removed presence of a default file name and default file extension
1818
* These also strongly felt out of place
1919
* ``load`` and ``_relative_load`` now require a file path
20-
* ``edit`` and ``save`` now use a temporary file if a file path isn't provided
20+
* ``edit`` and ``save`` now use a temporary file if a file path isn't provided
21+
* Load command has better error checking and reporting
22+
* Clipboard copy and paste functionality is now handled by the ``pyperclip`` module
2123

2224
0.7.3
2325
-----

CONTRIBUTING.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,12 @@ The tables below list all prerequisites along with the minimum required version
4242
4343
#### Prerequisites to run cmd2 applications
4444

45-
| Prerequisite | Minimum Version |
46-
| ------------------------------------------- | --------------- |
47-
| [Python](https://www.python.org/downloads/) | `3.3 or 2.7` |
48-
| [six](https://pypi.python.org/pypi/six) | `1.8` |
49-
| [pyparsing](http://pyparsing.wikispaces.com)| `2.0.3` |
45+
| Prerequisite | Minimum Version |
46+
| --------------------------------------------------- | --------------- |
47+
| [Python](https://www.python.org/downloads/) | `3.3 or 2.7` |
48+
| [six](https://pypi.python.org/pypi/six) | `1.8` |
49+
| [pyparsing](http://pyparsing.wikispaces.com) | `2.0.3` |
50+
| [pyperclip](https://github.com/asweigart/pyperclip) | `1.5` |
5051

5152
#### Additional prerequisites to run cmd2 unit tests
5253

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ cmd2
99

1010
cmd2 is a tool for writing command-line interactive applications for Python 2.7 and Python 3.3+. It is based on the
1111
Python Standard Library's [cmd](https://docs.python.org/3/library/cmd.html) module, and can be used any place cmd is used simply by importing cmd2 instead. It is
12-
pure Python code with the only 3rd-party dependencies being on [six](https://pypi.python.org/pypi/six) and [pyparsing](http://pyparsing.wikispaces.com).
12+
pure Python code with the only 3rd-party dependencies being on [six](https://pypi.python.org/pypi/six), [pyparsing](http://pyparsing.wikispaces.com),
13+
and [pyperclip](https://github.com/asweigart/pyperclip).
1314

1415
The latest documentation for cmd2 can be read online here: https://cmd2.readthedocs.io/en/latest/
1516

cmd2.py

Lines changed: 23 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
from optparse import make_option
4646

4747
import pyparsing
48+
import pyperclip
4849

4950
# next(it) gets next item of iterator it. This is a replacement for calling it.next() in Python 2 and next(it) in Py3
5051
from six import next
@@ -302,148 +303,37 @@ def new_func(instance, arg):
302303
return option_setup
303304

304305

305-
# Prefix to use on all OSes when the appropriate library or CLI tool isn't installed for getting access to paste buffer
306-
pastebufferr = """Redirecting to or from paste buffer requires %s
307-
to be installed on operating system.
308-
%s"""
309-
310-
# Can we access the clipboard?
311-
can_clip = False
312-
if sys.platform == "win32":
313-
# Running on Windows
306+
# Can we access the clipboard, always true on Windows and Mac, but only sometimes on Linux
307+
can_clip = True
308+
if sys.platform.startswith('linux'):
314309
try:
315-
# noinspection PyUnresolvedReferences
316-
import win32clipboard
317-
318-
def get_paste_buffer():
319-
"""Get the contents of the clipboard for Windows OSes.
320-
321-
:return: str - contents of the clipboard
322-
"""
323-
win32clipboard.OpenClipboard(0)
324-
try:
325-
result = win32clipboard.GetClipboardData()
326-
except TypeError:
327-
result = '' # non-text
328-
win32clipboard.CloseClipboard()
329-
return result
310+
pyperclip.paste()
311+
except pyperclip.exceptions.PyperclipException:
312+
can_clip = False
330313

331-
def write_to_paste_buffer(txt):
332-
"""Paste text to the clipboard for Windows OSes.
333314

334-
:param txt: str - text to paste to the clipboard
335-
"""
336-
win32clipboard.OpenClipboard(0)
337-
win32clipboard.EmptyClipboard()
338-
win32clipboard.SetClipboardText(txt)
339-
win32clipboard.CloseClipboard()
340-
341-
can_clip = True
342-
except ImportError:
343-
# noinspection PyUnusedLocal
344-
def get_paste_buffer(*args):
345-
"""For Windows OSes without the appropriate library installed to get text from clipboard, raise an exception.
346-
"""
347-
raise OSError(pastebufferr % ('pywin32', 'Download from http://sourceforge.net/projects/pywin32/'))
348-
349-
write_to_paste_buffer = get_paste_buffer
350-
elif sys.platform == 'darwin':
351-
# Running on Mac OS X
352-
try:
353-
# Warning: subprocess.call() and subprocess.check_call() should never be called with stdout=PIPE or stderr=PIPE
354-
# because the child process will block if it generates enough output to a pipe to fill up the OS pipe buffer.
355-
# Starting with Python 3.5 there is a newer, safer API based on the run() function.
356-
357-
# Python 3.3+ supports subprocess.DEVNULL, but that isn't defined for Python 2.7
358-
with open(os.devnull, 'w') as DEVNULL:
359-
# test for pbcopy - AFAIK, should always be installed on MacOS
360-
subprocess.check_call(['pbcopy', '-help'], stdin=subprocess.PIPE, stdout=DEVNULL, stderr=DEVNULL)
361-
can_clip = True
362-
except (subprocess.CalledProcessError, OSError, IOError):
363-
pass
364-
if can_clip:
365-
def get_paste_buffer():
366-
"""Get the contents of the clipboard for Mac OS X.
367-
368-
:return: str - contents of the clipboard
369-
"""
370-
pbcopyproc = subprocess.Popen('pbpaste', stdin=subprocess.PIPE, stdout=subprocess.PIPE,
371-
stderr=subprocess.PIPE)
372-
stdout, stderr = pbcopyproc.communicate()
373-
if six.PY3:
374-
return stdout.decode()
375-
else:
376-
return stdout
315+
def get_paste_buffer():
316+
"""Get the contents of the clipboard / paste buffer.
377317
378-
def write_to_paste_buffer(txt):
379-
"""Paste text to the clipboard for Mac OS X.
318+
:return: str - contents of the clipboard
319+
"""
320+
pb_unicode = pyperclip.paste()
380321

381-
:param txt: str - text to paste to the clipboard
382-
"""
383-
pbcopyproc = subprocess.Popen('pbcopy', stdin=subprocess.PIPE, stdout=subprocess.PIPE,
384-
stderr=subprocess.PIPE)
385-
if six.PY3:
386-
pbcopyproc.communicate(txt.encode())
387-
else:
388-
pbcopyproc.communicate(txt)
322+
if six.PY3:
323+
pb_str = pb_unicode
389324
else:
390-
# noinspection PyUnusedLocal
391-
def get_paste_buffer(*args):
392-
"""For Mac OS X without the appropriate tool installed to get text from clipboard, raise an exception."""
393-
raise OSError(pastebufferr % ('pbcopy',
394-
'On MacOS X - error should not occur - part of the default installation'))
395-
396-
write_to_paste_buffer = get_paste_buffer
397-
else:
398-
# Running on Linux
399-
try:
400-
with open(os.devnull, 'w') as DEVNULL:
401-
subprocess.check_call(['uptime', '|', 'xclip'], stdin=subprocess.PIPE, stdout=DEVNULL, stderr=DEVNULL)
402-
can_clip = True
403-
except (subprocess.CalledProcessError, OSError, IOError):
404-
pass # something went wrong with xclip and we cannot use it
405-
if can_clip:
406-
def get_paste_buffer():
407-
"""Get the contents of the clipboard for Linux OSes.
408-
409-
:return: str - contents of the clipboard
410-
"""
411-
xclipproc = subprocess.Popen(['xclip', '-o', '-selection', 'clipboard'], stdin=subprocess.PIPE,
412-
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
413-
stdout, stderr = xclipproc.communicate()
414-
if six.PY3:
415-
return stdout.decode()
416-
else:
417-
return stdout
325+
import unicodedata
326+
pb_str = unicodedata.normalize('NFKD', pb_unicode).encode('ascii', 'ignore')
418327

419-
def write_to_paste_buffer(txt):
420-
"""Paste text to the clipboard for Linux OSes.
328+
return pb_str
421329

422-
:param txt: str - text to paste to the clipboard
423-
"""
424-
xclipproc = subprocess.Popen(['xclip', '-selection', 'clipboard'], stdin=subprocess.PIPE,
425-
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
426-
if six.PY3:
427-
xclipproc.stdin.write(txt.encode())
428-
else:
429-
xclipproc.stdin.write(txt)
430-
xclipproc.stdin.close()
431-
432-
# but we want it in both the "primary" and "mouse" clipboards
433-
xclipproc = subprocess.Popen(['xclip'], stdin=subprocess.PIPE, stdout=subprocess.PIPE,
434-
stderr=subprocess.PIPE)
435-
if six.PY3:
436-
xclipproc.stdin.write(txt.encode())
437-
else:
438-
xclipproc.stdin.write(txt)
439-
xclipproc.stdin.close()
440-
else:
441-
# noinspection PyUnusedLocal
442-
def get_paste_buffer(*args):
443-
"""For Linux without the appropriate tool installed to get text from clipboard, raise an exception."""
444-
raise OSError(pastebufferr % ('xclip', 'On Debian/Ubuntu, install with "sudo apt-get install xclip"'))
445330

446-
write_to_paste_buffer = get_paste_buffer
331+
def write_to_paste_buffer(txt):
332+
"""Copy text to the clipboard / paste buffer.
333+
334+
:param txt: str - text to copy to the clipboard
335+
"""
336+
pyperclip.copy(txt)
447337

448338

449339
class ParsedString(str):

docs/install.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ the following Python packages are installed:
105105

106106
* six
107107
* pyparsing
108+
* pyperclip
108109

109110

110111
Upgrading cmd2

setup.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,10 @@
5656
Topic :: Software Development :: Libraries :: Python Modules
5757
""".splitlines())))
5858

59-
INSTALL_REQUIRES = ['pyparsing >= 2.0.1', 'six']
59+
INSTALL_REQUIRES = ['pyparsing >= 2.0.1', 'pyperclip', 'six']
6060
# unitest.mock was added in Python 3.3. mock is a backport of unittest.mock to all versions of Python
6161
TESTS_REQUIRE = ['mock', 'pytest']
62-
DOCS_REQUIRE = ['sphinx', 'sphinx_rtd_theme', 'pyparsing', 'six']
62+
DOCS_REQUIRE = ['sphinx', 'sphinx_rtd_theme', 'pyparsing', 'pyperclip', 'six']
6363

6464
setup(
6565
name="cmd2",

tests/test_cmd2.py

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -472,23 +472,20 @@ def test_pipe_to_shell(base_app):
472472
assert out[0].strip() == expected[0].strip()
473473

474474

475+
@pytest.mark.skipif(not cmd2.can_clip,
476+
reason="Pyperclip could not find a copy/paste mechanism for your system")
475477
def test_send_to_paste_buffer(base_app):
476478
from cmd2 import can_clip
477479

478480
# Test writing to the PasteBuffer/Clipboard
479481
run_cmd(base_app, 'help >')
480482
expected = normalize(BASE_HELP)
481-
482-
# If the tools for interacting with the clipboard/pastebuffer are available
483-
if cmd2.can_clip:
484-
# Read from the clipboard
485-
assert normalize(cmd2.get_paste_buffer()) == expected
483+
assert normalize(cmd2.get_paste_buffer()) == expected
486484

487485
# Test appending to the PasteBuffer/Clipboard
488486
run_cmd(base_app, 'help history >>')
489487
expected = normalize(BASE_HELP + '\n' + HELP_HISTORY)
490-
if cmd2.can_clip:
491-
assert normalize(cmd2.get_paste_buffer()) == expected
488+
assert normalize(cmd2.get_paste_buffer()) == expected
492489

493490

494491
def test_base_timing(base_app, capsys):

tox.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ deps =
77
codecov
88
mock
99
pyparsing
10+
pyperclip
1011
pytest
1112
pytest-cov
1213
six

0 commit comments

Comments
 (0)