Skip to content

Commit 45d2398

Browse files
committed
Merge branch 'master' into command_help_noflag
2 parents 7cc94ab + dac0311 commit 45d2398

19 files changed

+326
-336
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@
99
for formatting help/description text
1010
* Aliases are now sorted alphabetically
1111
* The **set** command now tab-completes settable parameter names
12+
* Deletions
13+
* The ``preparse``, ``postparsing_precmd``, and ``postparsing_postcmd`` methods *deprecated* in the previous release
14+
have been deleted
15+
* The new application lifecycle hook system allows for registration of callbacks to be called at various points
16+
in the lifecycle and is more powerful and flexible than the previous system
1217

1318
## 0.9.4 (August 21, 2018)
1419
* Bug Fixes

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ applications. It provides a simple API which is an extension of Python's built-
1414
of cmd to make your life easier and eliminates much of the boilerplate code which would be necessary
1515
when using cmd.
1616

17-
[![Screenshot](cmd2.png)](https://github.com/python-cmd2/cmd2/blob/master/cmd2.png)
18-
17+
Click on image below to watch a short video demonstrating the capabilities of cmd2:
18+
[![Screenshot](cmd2.png)](https://youtu.be/DDU_JH6cFsA)
1919

2020
Main Features
2121
-------------

cmd2.png

17.5 KB
Loading

cmd2/cmd2.py

Lines changed: 18 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1649,58 +1649,6 @@ def precmd(self, statement: Statement) -> Statement:
16491649
"""
16501650
return statement
16511651

1652-
# ----- Methods which are cmd2-specific lifecycle hooks which are not present in cmd -----
1653-
1654-
# noinspection PyMethodMayBeStatic
1655-
def preparse(self, raw: str) -> str:
1656-
"""Hook method executed before user input is parsed.
1657-
1658-
WARNING: If it's a multiline command, `preparse()` may not get all the
1659-
user input. _complete_statement() really does two things: a) parse the
1660-
user input, and b) accept more input in case it's a multiline command
1661-
the passed string doesn't have a terminator. `preparse()` is currently
1662-
called before we know whether it's a multiline command, and before we
1663-
know whether the user input includes a termination character.
1664-
1665-
If you want a reliable pre parsing hook method, register a postparsing
1666-
hook, modify the user input, and then reparse it.
1667-
1668-
:param raw: raw command line input :return: potentially modified raw command line input
1669-
:return: a potentially modified version of the raw input string
1670-
"""
1671-
return raw
1672-
1673-
# noinspection PyMethodMayBeStatic
1674-
def postparsing_precmd(self, statement: Statement) -> Tuple[bool, Statement]:
1675-
"""This runs after parsing the command-line, but before anything else; even before adding cmd to history.
1676-
1677-
NOTE: This runs before precmd() and prior to any potential output redirection or piping.
1678-
1679-
If you wish to fatally fail this command and exit the application entirely, set stop = True.
1680-
1681-
If you wish to just fail this command you can do so by raising an exception:
1682-
1683-
- raise EmptyStatement - will silently fail and do nothing
1684-
- raise <AnyOtherException> - will fail and print an error message
1685-
1686-
:param statement: the parsed command-line statement as a Statement object
1687-
:return: (stop, statement) containing a potentially modified version of the statement object
1688-
"""
1689-
stop = False
1690-
return stop, statement
1691-
1692-
# noinspection PyMethodMayBeStatic
1693-
def postparsing_postcmd(self, stop: bool) -> bool:
1694-
"""This runs after everything else, including after postcmd().
1695-
1696-
It even runs when an empty line is entered. Thus, if you need to do something like update the prompt due
1697-
to notifications from a background thread, then this is the method you want to override to do it.
1698-
1699-
:param stop: True implies the entire application should exit.
1700-
:return: True implies the entire application should exit.
1701-
"""
1702-
return stop
1703-
17041652
def parseline(self, line: str) -> Tuple[str, str, str]:
17051653
"""Parse the line into a command name and a string containing the arguments.
17061654
@@ -1740,9 +1688,6 @@ def onecmd_plus_hooks(self, line: str) -> bool:
17401688
data = func(data)
17411689
if data.stop:
17421690
break
1743-
# postparsing_precmd is deprecated
1744-
if not data.stop:
1745-
(data.stop, data.statement) = self.postparsing_precmd(data.statement)
17461691
# unpack the data object
17471692
statement = data.statement
17481693
stop = data.stop
@@ -1807,9 +1752,7 @@ def _run_cmdfinalization_hooks(self, stop: bool, statement: Optional[Statement])
18071752
data = func(data)
18081753
# retrieve the final value of stop, ignoring any
18091754
# modifications to the statement
1810-
stop = data.stop
1811-
# postparsing_postcmd is deprecated
1812-
return self.postparsing_postcmd(stop)
1755+
return data.stop
18131756
except Exception as ex:
18141757
self.perror(ex)
18151758

@@ -1863,9 +1806,6 @@ def _complete_statement(self, line: str) -> Statement:
18631806
pipe runs out. We can't refactor it because we need to retain
18641807
backwards compatibility with the standard library version of cmd.
18651808
"""
1866-
# preparse() is deprecated, use self.register_postparsing_hook() instead
1867-
line = self.preparse(line)
1868-
18691809
while True:
18701810
try:
18711811
statement = self.statement_parser.parse(line)
@@ -2220,7 +2160,7 @@ def _cmdloop(self) -> bool:
22202160
def do_alias(self, statement: Statement) -> None:
22212161
"""Define or display aliases
22222162
2223-
Usage: Usage: alias [name] | [<name> <value>]
2163+
Usage: alias [name] | [<name> <value>]
22242164
Where:
22252165
name - name of the alias being looked up, added, or replaced
22262166
value - what the alias will be resolved to (if adding or replacing)
@@ -2248,7 +2188,8 @@ def do_alias(self, statement: Statement) -> None:
22482188

22492189
# If no args were given, then print a list of current aliases
22502190
if not alias_arg_list:
2251-
for cur_alias in self.aliases:
2191+
sorted_aliases = utils.alphabetical_sort(list(self.aliases))
2192+
for cur_alias in sorted_aliases:
22522193
self.poutput("alias {} {}".format(cur_alias, self.aliases[cur_alias]))
22532194
return
22542195

@@ -2282,9 +2223,6 @@ def do_alias(self, statement: Statement) -> None:
22822223
# Set the alias
22832224
self.aliases[name] = value
22842225
self.poutput("Alias {!r} created".format(name))
2285-
2286-
# Keep aliases in alphabetically sorted order
2287-
self.aliases = collections.OrderedDict(sorted(self.aliases.items()))
22882226
else:
22892227
errmsg = "Aliases can not contain: {}".format(invalidchars)
22902228
self.perror(errmsg, traceback_war=False)
@@ -2305,7 +2243,7 @@ def complete_alias(self, text: str, line: str, begidx: int, endidx: int) -> List
23052243
def do_unalias(self, arglist: List[str]) -> None:
23062244
"""Unsets aliases
23072245
2308-
Usage: Usage: unalias [-a] name [name ...]
2246+
Usage: unalias [-a] name [name ...]
23092247
Where:
23102248
name - name of the alias being unset
23112249
@@ -2454,13 +2392,17 @@ def _print_topics(self, header: str, cmds: List[str], verbose: bool) -> None:
24542392
doc_block = []
24552393
found_first = False
24562394
for doc_line in doc.splitlines():
2457-
str(doc_line).strip()
2458-
if len(doc_line.strip()) > 0:
2459-
doc_block.append(doc_line.strip())
2460-
found_first = True
2461-
else:
2395+
stripped_line = doc_line.strip()
2396+
2397+
# Don't include :param type lines
2398+
if stripped_line.startswith(':'):
24622399
if found_first:
24632400
break
2401+
elif stripped_line:
2402+
doc_block.append(stripped_line)
2403+
found_first = True
2404+
elif found_first:
2405+
break
24642406

24652407
for doc_line in doc_block:
24662408
self.stdout.write('{: <{col_width}}{doc}\n'.format(command,
@@ -2686,9 +2628,11 @@ def do_py(self, arg: str) -> bool:
26862628
Non-python commands can be issued with ``pyscript_name("your command")``.
26872629
Run python code from external script files with ``run("script.py")``
26882630
"""
2689-
from .pyscript_bridge import PyscriptBridge
2631+
from .pyscript_bridge import PyscriptBridge, CommandResult
26902632
if self._in_py:
2691-
self.perror("Recursively entering interactive Python consoles is not allowed.", traceback_war=False)
2633+
err = "Recursively entering interactive Python consoles is not allowed."
2634+
self.perror(err, traceback_war=False)
2635+
self._last_result = CommandResult('', err)
26922636
return False
26932637
self._in_py = True
26942638

cmd2/pyscript_bridge.py

Lines changed: 8 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,15 @@
1212
import sys
1313
from typing import List, Callable
1414

15+
from .argparse_completer import _RangeAction
16+
from .utils import namedtuple_with_defaults, StdSim
17+
1518
# Python 3.4 require contextlib2 for temporarily redirecting stderr and stdout
1619
if sys.version_info < (3, 5):
1720
from contextlib2 import redirect_stdout, redirect_stderr
1821
else:
1922
from contextlib import redirect_stdout, redirect_stderr
2023

21-
from .argparse_completer import _RangeAction
22-
from .utils import namedtuple_with_defaults
23-
2424

2525
class CommandResult(namedtuple_with_defaults('CommandResult', ['stdout', 'stderr', 'data'])):
2626
"""Encapsulates the results from a command.
@@ -38,37 +38,12 @@ def __bool__(self):
3838
return not self.stderr and self.data is not None
3939

4040

41-
class CopyStream(object):
42-
"""Copies all data written to a stream"""
43-
def __init__(self, inner_stream, echo: bool = False) -> None:
44-
self.buffer = ''
45-
self.inner_stream = inner_stream
46-
self.echo = echo
47-
48-
def write(self, s):
49-
self.buffer += s
50-
if self.echo:
51-
self.inner_stream.write(s)
52-
53-
def read(self):
54-
raise NotImplementedError
55-
56-
def clear(self):
57-
self.buffer = ''
58-
59-
def __getattr__(self, item: str):
60-
if item in self.__dict__:
61-
return self.__dict__[item]
62-
else:
63-
return getattr(self.inner_stream, item)
64-
65-
6641
def _exec_cmd(cmd2_app, func: Callable, echo: bool):
6742
"""Helper to encapsulate executing a command and capturing the results"""
68-
copy_stdout = CopyStream(sys.stdout, echo)
69-
copy_stderr = CopyStream(sys.stderr, echo)
43+
copy_stdout = StdSim(sys.stdout, echo)
44+
copy_stderr = StdSim(sys.stderr, echo)
7045

71-
copy_cmd_stdout = CopyStream(cmd2_app.stdout, echo)
46+
copy_cmd_stdout = StdSim(cmd2_app.stdout, echo)
7247

7348
cmd2_app._last_result = None
7449

@@ -81,9 +56,9 @@ def _exec_cmd(cmd2_app, func: Callable, echo: bool):
8156
cmd2_app.stdout = copy_cmd_stdout.inner_stream
8257

8358
# if stderr is empty, set it to None
84-
stderr = copy_stderr.buffer if copy_stderr.buffer else None
59+
stderr = copy_stderr.getvalue() if copy_stderr.getvalue() else None
8560

86-
outbuf = copy_cmd_stdout.buffer if copy_cmd_stdout.buffer else copy_stdout.buffer
61+
outbuf = copy_cmd_stdout.getvalue() if copy_cmd_stdout.getvalue() else copy_stdout.getvalue()
8762
result = CommandResult(stdout=outbuf, stderr=stderr, data=cmd2_app._last_result)
8863
return result
8964

cmd2/transcript.py

Lines changed: 1 addition & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ def setUp(self):
4444

4545
# Trap stdout
4646
self._orig_stdout = self.cmdapp.stdout
47-
self.cmdapp.stdout = OutputTrap()
47+
self.cmdapp.stdout = utils.StdSim(self.cmdapp.stdout)
4848

4949
def runTest(self): # was testall
5050
if self.cmdapp:
@@ -203,24 +203,3 @@ def tearDown(self):
203203
if self.cmdapp:
204204
# Restore stdout
205205
self.cmdapp.stdout = self._orig_stdout
206-
207-
class OutputTrap(object):
208-
"""Instantiate an OutputTrap to divert/capture ALL stdout output.
209-
For use in transcript testing.
210-
"""
211-
212-
def __init__(self):
213-
self.contents = ''
214-
215-
def write(self, txt: str):
216-
"""Add text to the internal contents."""
217-
self.contents += txt
218-
219-
def read(self) -> str:
220-
"""Read from the internal contents and then clear them out.
221-
222-
:return: str - text from the internal contents
223-
"""
224-
result = self.contents
225-
self.contents = ''
226-
return result

cmd2/utils.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,3 +246,61 @@ def natural_sort(list_to_sort: List[str]) -> List[str]:
246246
:return: the list sorted naturally
247247
"""
248248
return sorted(list_to_sort, key=natural_keys)
249+
250+
251+
class StdSim(object):
252+
"""Class to simulate behavior of sys.stdout or sys.stderr.
253+
254+
Stores contents in internal buffer and optionally echos to the inner stream it is simulating.
255+
"""
256+
class ByteBuf(object):
257+
"""Inner class which stores an actual bytes buffer and does the actual output if echo is enabled."""
258+
def __init__(self, inner_stream, echo: bool = False) -> None:
259+
self.byte_buf = b''
260+
self.inner_stream = inner_stream
261+
self.echo = echo
262+
263+
def write(self, b: bytes) -> None:
264+
"""Add bytes to internal bytes buffer and if echo is True, echo contents to inner stream."""
265+
if not isinstance(b, bytes):
266+
raise TypeError('a bytes-like object is required, not {}'.format(type(b)))
267+
self.byte_buf += b
268+
if self.echo:
269+
self.inner_stream.buffer.write(b)
270+
271+
def __init__(self, inner_stream, echo: bool = False) -> None:
272+
self.buffer = self.ByteBuf(inner_stream, echo)
273+
self.inner_stream = inner_stream
274+
275+
def write(self, s: str) -> None:
276+
"""Add str to internal bytes buffer and if echo is True, echo contents to inner stream."""
277+
if not isinstance(s, str):
278+
raise TypeError('write() argument must be str, not {}'.format(type(s)))
279+
b = s.encode()
280+
self.buffer.write(b)
281+
282+
def getvalue(self) -> str:
283+
"""Get the internal contents as a str.
284+
285+
:return string from the internal contents
286+
"""
287+
return self.buffer.byte_buf.decode()
288+
289+
def read(self) -> str:
290+
"""Read from the internal contents as a str and then clear them out.
291+
292+
:return: string from the internal contents
293+
"""
294+
result = self.getvalue()
295+
self.clear()
296+
return result
297+
298+
def clear(self) -> None:
299+
"""Clear the internal contents."""
300+
self.buffer.byte_buf = b''
301+
302+
def __getattr__(self, item: str):
303+
if item in self.__dict__:
304+
return self.__dict__[item]
305+
else:
306+
return getattr(self.inner_stream, item)

0 commit comments

Comments
 (0)