Skip to content

Commit 8427886

Browse files
authored
Merge branch 'master' into autocompleter
2 parents f0c98ac + 3c8880e commit 8427886

14 files changed

+309
-112
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,7 @@ htmlcov
2020

2121
# mypy optional static type checker
2222
.mypy_cache
23+
24+
# mypy plugin for PyCharm
25+
dmypy.json
26+
dmypy.sock

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,17 @@
33
* Fixed issue where piping and redirecting did not work correctly with paths that had spaces
44
* Enhancements
55
* Added ability to print a header above tab-completion suggestions using `completion_header` member
6+
* Added ``pager`` and ``pager_chop`` attributes to the ``cmd2.Cmd`` class
7+
* ``pager`` defaults to **less -RXF** on POSIX and **more** on Windows
8+
* ``pager_chop`` defaults to **less -SRXF** on POSIX and **more** on Windows
9+
* Added ``chop`` argument to ``cmd2.Cmd.ppaged()`` method for displaying output using a pager
10+
* If ``chop`` is ``False``, then ``self.pager`` is used as the pager
11+
* Otherwise ``self.pager_chop`` is used as the pager
12+
* Deprecations
13+
* The ``CmdResult`` helper class is *deprecated* and replaced by the improved ``CommandResult`` class
14+
* ``CommandResult`` has the following attributes: **stdout**, **stderr**, and **data**
15+
* ``CmdResult`` had attributes of: **out**, **err**, **war**
16+
* ``CmdResult`` will be deleted in the next release
617

718
## 0.8.8 (TBD, 2018)
819
* Bug Fixes

CODEOWNERS

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,43 @@
1212

1313
# You can also use email addresses if you prefer.
1414
15+
16+
# cmd2 code
17+
cmd2/__init__.py @tleonhardt @kotfu
18+
cmd2/arg*.py @anselor
19+
cmd2/cmd2.py @tleonhardt @kmvanbrunt @kotfu
20+
cmd2/constants.py @kotfu
21+
cmd2/parsing.py @kotfu @kmvanbrunt
22+
cmd2/pyscript*.py @anselor
23+
cmd2/rl_utils.py @kmvanbrunt
24+
cmd2/transcript.py @kotfu
25+
cmd2/utils.py @tleonhardt @kotfu @kmvanbrunt
26+
27+
# Sphinx documentation
28+
docs/* @tleonhardt @kotfu
29+
30+
# Examples
31+
examples/env*.py @kotfu
32+
examples/help*.py @anselor
33+
examples/tab_au*.py @anselor
34+
examples/tab_co*.py @kmvanbrunt
35+
36+
# Unit Tests
37+
tests/pyscript/* @anselor
38+
tests/transcripts/* @kotfu
39+
tests/__init__.py @kotfu
40+
tests/conftest.py @kotfu @tleonhardt
41+
tests/test_acar*.py @anselor
42+
tests/test_argp*.py @kotfu
43+
tests/test_auto*.py @anselor
44+
tests/test_bash*.py @anselor @tleonhardt
45+
tests/test_comp*.py @kmvanbrunt
46+
tests/test_pars*.py @kotfu
47+
tests/test_pysc*.py @anselor
48+
tests/test_tran*.py @kotfu
49+
50+
# Top-level project stuff
51+
CONTRIBUTING.md @tleonhardt @kotfu
52+
setup.py @tleonhardt @kotfu
53+
tasks.py @kotfu
54+

cmd2/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#
22
# -*- coding: utf-8 -*-
33
"""This simply imports certain things for backwards compatibility."""
4-
from .cmd2 import __version__, Cmd, CmdResult, Statement, EmptyStatement, categorize
4+
from .cmd2 import __version__, Cmd, Statement, EmptyStatement, categorize
55
from .cmd2 import with_argument_list, with_argparser, with_argparser_and_unknown_args, with_category
6+
from .pyscript_bridge import CommandResult

cmd2/clipboard.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# coding=utf-8
2+
"""
3+
This module provides basic ability to copy from and paste to the clipboard/pastebuffer.
4+
"""
5+
import sys
6+
7+
import pyperclip
8+
9+
# Newer versions of pyperclip are released as a single file, but older versions had a more complicated structure
10+
try:
11+
from pyperclip.exceptions import PyperclipException
12+
except ImportError: # pragma: no cover
13+
# noinspection PyUnresolvedReferences
14+
from pyperclip import PyperclipException
15+
16+
# Can we access the clipboard? Should always be true on Windows and Mac, but only sometimes on Linux
17+
# noinspection PyUnresolvedReferences
18+
try:
19+
# Get the version of the pyperclip module as a float
20+
pyperclip_ver = float('.'.join(pyperclip.__version__.split('.')[:2]))
21+
22+
# The extraneous output bug in pyperclip on Linux using xclip was fixed in more recent versions of pyperclip
23+
if sys.platform.startswith('linux') and pyperclip_ver < 1.6:
24+
# Avoid extraneous output to stderr from xclip when clipboard is empty at cost of overwriting clipboard contents
25+
pyperclip.copy('')
26+
else:
27+
# Try getting the contents of the clipboard
28+
_ = pyperclip.paste()
29+
except PyperclipException:
30+
can_clip = False
31+
else:
32+
can_clip = True
33+
34+
35+
def get_paste_buffer() -> str:
36+
"""Get the contents of the clipboard / paste buffer.
37+
38+
:return: contents of the clipboard
39+
"""
40+
pb_str = pyperclip.paste()
41+
return pb_str
42+
43+
44+
def write_to_paste_buffer(txt: str) -> None:
45+
"""Copy text to the clipboard / paste buffer.
46+
47+
:param txt: text to copy to the clipboard
48+
"""
49+
pyperclip.copy(txt)

cmd2/cmd2.py

Lines changed: 36 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -41,16 +41,15 @@
4141
import sys
4242
from typing import Any, Callable, Dict, List, Mapping, Optional, Tuple, Union
4343

44-
import pyperclip
45-
4644
from . import constants
4745
from . import utils
48-
49-
from cmd2.parsing import StatementParser, Statement
46+
from .argparse_completer import AutoCompleter, ACArgumentParser
47+
from .clipboard import can_clip, get_paste_buffer, write_to_paste_buffer
48+
from .parsing import StatementParser, Statement
5049

5150
# Set up readline
5251
from .rl_utils import rl_type, RlType
53-
if rl_type == RlType.NONE: # pragma: no cover
52+
if rl_type == RlType.NONE: # pragma: no cover
5453
rl_warning = "Readline features including tab completion have been disabled since no \n" \
5554
"supported version of readline was found. To resolve this, install \n" \
5655
"pyreadline on Windows or gnureadline on Mac.\n\n"
@@ -79,15 +78,6 @@
7978
rl_basic_quote_characters = ctypes.c_char_p.in_dll(readline_lib, "rl_basic_quote_characters")
8079
orig_rl_basic_quotes = ctypes.cast(rl_basic_quote_characters, ctypes.c_void_p).value
8180

82-
from .argparse_completer import AutoCompleter, ACArgumentParser
83-
84-
# Newer versions of pyperclip are released as a single file, but older versions had a more complicated structure
85-
try:
86-
from pyperclip.exceptions import PyperclipException
87-
except ImportError: # pragma: no cover
88-
# noinspection PyUnresolvedReferences
89-
from pyperclip import PyperclipException
90-
9181
# Collection is a container that is sizable and iterable
9282
# It was introduced in Python 3.6. We will try to import it, otherwise use our implementation
9383
try:
@@ -121,7 +111,7 @@ def __subclasshook__(cls, C):
121111
try:
122112
# noinspection PyUnresolvedReferences,PyPackageRequirements
123113
from IPython import embed
124-
except ImportError: # pragma: no cover
114+
except ImportError: # pragma: no cover
125115
ipython_available = False
126116

127117
__version__ = '0.9.2a'
@@ -271,48 +261,6 @@ def cmd_wrapper(instance, cmdline):
271261
return arg_decorator
272262

273263

274-
# Can we access the clipboard? Should always be true on Windows and Mac, but only sometimes on Linux
275-
# noinspection PyUnresolvedReferences
276-
try:
277-
# Get the version of the pyperclip module as a float
278-
pyperclip_ver = float('.'.join(pyperclip.__version__.split('.')[:2]))
279-
280-
# The extraneous output bug in pyperclip on Linux using xclip was fixed in more recent versions of pyperclip
281-
if sys.platform.startswith('linux') and pyperclip_ver < 1.6:
282-
# Avoid extraneous output to stderr from xclip when clipboard is empty at cost of overwriting clipboard contents
283-
pyperclip.copy('')
284-
else:
285-
# Try getting the contents of the clipboard
286-
_ = pyperclip.paste()
287-
except PyperclipException:
288-
can_clip = False
289-
else:
290-
can_clip = True
291-
292-
293-
def disable_clip() -> None:
294-
""" Allows user of cmd2 to manually disable clipboard cut-and-paste functionality."""
295-
global can_clip
296-
can_clip = False
297-
298-
299-
def get_paste_buffer() -> str:
300-
"""Get the contents of the clipboard / paste buffer.
301-
302-
:return: contents of the clipboard
303-
"""
304-
pb_str = pyperclip.paste()
305-
return pb_str
306-
307-
308-
def write_to_paste_buffer(txt: str) -> None:
309-
"""Copy text to the clipboard / paste buffer.
310-
311-
:param txt: text to copy to the clipboard
312-
"""
313-
pyperclip.copy(txt)
314-
315-
316264
class EmbeddedConsoleExit(SystemExit):
317265
"""Custom exception class for use with the py command."""
318266
pass
@@ -356,7 +304,6 @@ class Cmd(cmd.Cmd):
356304
Line-oriented command interpreters are often useful for test harnesses, internal tools, and rapid prototypes.
357305
"""
358306
# Attributes used to configure the StatementParser, best not to change these at runtime
359-
blankLinesAllowed = False
360307
multiline_commands = []
361308
shortcuts = {'?': 'help', '!': 'shell', '@': 'load', '@@': '_relative_load'}
362309
aliases = dict()
@@ -505,7 +452,7 @@ def __init__(self, completekey: str='tab', stdin=None, stdout=None, persistent_h
505452
if startup_script is not None:
506453
startup_script = os.path.expanduser(startup_script)
507454
if os.path.exists(startup_script) and os.path.getsize(startup_script) > 0:
508-
self.cmdqueue.append('load {}'.format(startup_script))
455+
self.cmdqueue.append("load '{}'".format(startup_script))
509456

510457
############################################################################################################
511458
# The following variables are used by tab-completion functions. They are reset each time complete() is run
@@ -534,6 +481,21 @@ def __init__(self, completekey: str='tab', stdin=None, stdout=None, persistent_h
534481
# quote matches that are completed in a delimited fashion
535482
self.matches_delimited = False
536483

484+
# Set the pager(s) for use with the ppaged() method for displaying output using a pager
485+
if sys.platform.startswith('win'):
486+
self.pager = self.pager_chop = 'more'
487+
else:
488+
# Here is the meaning of the various flags we are using with the less command:
489+
# -S causes lines longer than the screen width to be chopped (truncated) rather than wrapped
490+
# -R causes ANSI "color" escape sequences to be output in raw form (i.e. colors are displayed)
491+
# -X disables sending the termcap initialization and deinitialization strings to the terminal
492+
# -F causes less to automatically exit if the entire file can be displayed on the first screen
493+
self.pager = 'less -RXF'
494+
self.pager_chop = 'less -SRXF'
495+
496+
# This boolean flag determines whether or not the cmd2 application can interact with the clipboard
497+
self.can_clip = can_clip
498+
537499
# ----- Methods related to presenting output to the user -----
538500

539501
@property
@@ -608,14 +570,20 @@ def pfeedback(self, msg: str) -> None:
608570
else:
609571
sys.stderr.write("{}\n".format(msg))
610572

611-
def ppaged(self, msg: str, end: str='\n') -> None:
573+
def ppaged(self, msg: str, end: str='\n', chop: bool=False) -> None:
612574
"""Print output using a pager if it would go off screen and stdout isn't currently being redirected.
613575
614576
Never uses a pager inside of a script (Python or text) or when output is being redirected or piped or when
615577
stdout or stdin are not a fully functional terminal.
616578
617-
:param msg: str - message to print to current stdout - anything convertible to a str with '{}'.format() is OK
618-
:param end: str - string appended after the end of the message if not already present, default a newline
579+
:param msg: message to print to current stdout - anything convertible to a str with '{}'.format() is OK
580+
:param end: string appended after the end of the message if not already present, default a newline
581+
:param chop: True -> causes lines longer than the screen width to be chopped (truncated) rather than wrapped
582+
- truncated text is still accessible by scrolling with the right & left arrow keys
583+
- chopping is ideal for displaying wide tabular data as is done in utilities like pgcli
584+
False -> causes lines longer than the screen width to wrap to the next line
585+
- wrapping is ideal when you want to avoid users having to use horizontal scrolling
586+
WARNING: On Windows, the text always wraps regardless of what the chop argument is set to
619587
"""
620588
import subprocess
621589
if msg is not None and msg != '':
@@ -635,17 +603,10 @@ def ppaged(self, msg: str, end: str='\n') -> None:
635603
# Don't attempt to use a pager that can block if redirecting or running a script (either text or Python)
636604
# Also only attempt to use a pager if actually running in a real fully functional terminal
637605
if functional_terminal and not self.redirecting and not self._in_py and not self._script_dir:
638-
639-
if sys.platform.startswith('win'):
640-
pager_cmd = 'more'
641-
else:
642-
# Here is the meaning of the various flags we are using with the less command:
643-
# -S causes lines longer than the screen width to be chopped (truncated) rather than wrapped
644-
# -R causes ANSI "color" escape sequences to be output in raw form (i.e. colors are displayed)
645-
# -X disables sending the termcap initialization and deinitialization strings to the terminal
646-
# -F causes less to automatically exit if the entire file can be displayed on the first screen
647-
pager_cmd = 'less -SRXF'
648-
self.pipe_proc = subprocess.Popen(pager_cmd, shell=True, stdin=subprocess.PIPE)
606+
pager = self.pager
607+
if chop:
608+
pager = self.pager_chop
609+
self.pipe_proc = subprocess.Popen(pager, shell=True, stdin=subprocess.PIPE)
649610
try:
650611
self.pipe_proc.stdin.write(msg_str.encode('utf-8', 'replace'))
651612
self.pipe_proc.stdin.close()
@@ -1870,7 +1831,7 @@ def _redirect_output(self, statement: Statement) -> None:
18701831
raise ex
18711832
elif statement.output:
18721833
import tempfile
1873-
if (not statement.output_to) and (not can_clip):
1834+
if (not statement.output_to) and (not self.can_clip):
18741835
raise EnvironmentError("Cannot redirect to paste buffer; install 'pyperclip' and re-run to enable")
18751836
self.kept_state = Statekeeper(self, ('stdout',))
18761837
self.kept_sys = Statekeeper(sys, ('stdout',))
@@ -3257,7 +3218,7 @@ def restore(self) -> None:
32573218

32583219

32593220
class CmdResult(utils.namedtuple_with_two_defaults('CmdResult', ['out', 'err', 'war'])):
3260-
"""Derive a class to store results from a named tuple so we can tweak dunder methods for convenience.
3221+
"""DEPRECATED: Derive a class to store results from a named tuple so we can tweak dunder methods for convenience.
32613222
32623223
This is provided as a convenience and an example for one possible way for end users to store results in
32633224
the self._last_result attribute of cmd2.Cmd class instances. See the "python_scripting.py" example for how it can

cmd2/pyscript_bridge.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
from .utils import namedtuple_with_defaults
2323

2424

25-
class CommandResult(namedtuple_with_defaults('CmdResult', ['stdout', 'stderr', 'data'])):
25+
class CommandResult(namedtuple_with_defaults('CommandResult', ['stdout', 'stderr', 'data'])):
2626
"""Encapsulates the results from a command.
2727
2828
Named tuple attributes

cmd2/rl_utils.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,22 +75,24 @@ def pyreadline_remove_history_item(pos: int) -> None:
7575
readline_lib = ctypes.CDLL(readline.__file__)
7676

7777

78+
# noinspection PyProtectedMember
7879
def rl_force_redisplay() -> None:
7980
"""
80-
Causes readline to redraw prompt and input line
81+
Causes readline to display the prompt and input text wherever the cursor is and start
82+
reading input from this location. This is the proper way to restore the input line after
83+
printing to the screen
8184
"""
8285
if not sys.stdout.isatty():
8386
return
8487

8588
if rl_type == RlType.GNU: # pragma: no cover
86-
# rl_forced_update_display() is the proper way to redraw the prompt and line, but we
87-
# have to use ctypes to do it since Python's readline API does not wrap the function
8889
readline_lib.rl_forced_update_display()
8990

9091
# After manually updating the display, readline asks that rl_display_fixed be set to 1 for efficiency
9192
display_fixed = ctypes.c_int.in_dll(readline_lib, "rl_display_fixed")
9293
display_fixed.value = 1
9394

9495
elif rl_type == RlType.PYREADLINE: # pragma: no cover
95-
# noinspection PyProtectedMember
96+
# Call _print_prompt() first to set the new location of the prompt
9697
readline.rl.mode._print_prompt()
98+
readline.rl.mode._update_line()

docs/argument_processing.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -333,7 +333,7 @@ Here's what it looks like::
333333
if unknown:
334334
self.perror("dir does not take any positional arguments:", traceback_war=False)
335335
self.do_help('dir')
336-
self._last_result = CmdResult('', 'Bad arguments')
336+
self._last_result = CommandResult('', 'Bad arguments')
337337
return
338338

339339
# Get the contents as a list

0 commit comments

Comments
 (0)