summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKevin Van Brunt <kmvanbrunt@gmail.com>2021-01-29 20:07:46 -0500
committerKevin Van Brunt <kmvanbrunt@gmail.com>2021-01-29 20:07:46 -0500
commit434a01f44e7d2302b4deef8f2e8069cbc26df560 (patch)
treecb3d5425668954fdd92989121ab53741c56908f8
parent82fe1d473ca2fe0278568036659fa78cb3c17f78 (diff)
parentcd377071cd122bc92a829322e00ae43fd5a5c254 (diff)
downloadcmd2-git-434a01f44e7d2302b4deef8f2e8069cbc26df560.tar.gz
Merge branch 'master' into 2.0
-rw-r--r--CHANGELOG.md3
-rw-r--r--Pipfile3
-rw-r--r--cmd2/cmd2.py8
-rw-r--r--cmd2/rl_utils.py4
-rw-r--r--cmd2/table_creator.py48
-rw-r--r--docs/features/commands.rst4
-rw-r--r--docs/features/modular_commands.rst2
-rw-r--r--docs/features/startup_commands.rst9
-rw-r--r--docs/overview/integrating.rst9
-rwxr-xr-xsetup.py5
-rwxr-xr-xtests/test_history.py13
-rw-r--r--tests/test_table_creator.py46
12 files changed, 112 insertions, 42 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5dacc331..a1abed8f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -20,9 +20,12 @@
* Bug Fixes
* Fixed bug where setting `always_show_hint=True` did not show a hint when completing `Settables`
* Fixed bug in editor detection logic on Linux systems that do not have `which`
+ * Fixed bug in table creator where column headers with tabs would result in an incorrect width calculation
+ * Fixed `FileNotFoundError` which occurred when running `history --clear` and no history file existed.
* Enhancements
* Added `silent_startup_script` option to `cmd2.Cmd.__init__()`. If `True`, then the startup script's
output will be suppressed. Anything written to stderr will still display.
+ * cmd2 now uses pyreadline3 when running Python 3.8 or greater on Windows
## 1.4.0 (November 11, 2020)
* Bug Fixes
diff --git a/Pipfile b/Pipfile
index b384709c..bfa222b6 100644
--- a/Pipfile
+++ b/Pipfile
@@ -22,7 +22,8 @@ ipython = "*"
isort = "*"
mock = {version = "*",markers = "python_version < '3.6'"}
plumbum = "*"
-pyreadline = {version = "*",sys_platform = "== 'win32'"}
+pyreadline = {version = "*",sys_platform = "== 'win32'",markers = "python_version < '3.8'"}
+pyreadline3 = {version = "*",sys_platform = "== 'win32'",markers = "python_version >= '3.8'"}
pytest = "*"
pytest-cov = "*"
pytest-mock = "*"
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py
index ef82c196..27746323 100644
--- a/cmd2/cmd2.py
+++ b/cmd2/cmd2.py
@@ -4049,7 +4049,13 @@ class Cmd(cmd.Cmd):
self.history.clear()
if self.persistent_history_file:
- os.remove(self.persistent_history_file)
+ try:
+ os.remove(self.persistent_history_file)
+ except FileNotFoundError:
+ pass
+ except OSError as ex:
+ self.pexcept("Error removing history file '{}': {}".format(self.persistent_history_file, ex))
+ return
if rl_type != RlType.NONE:
readline.clear_history()
diff --git a/cmd2/rl_utils.py b/cmd2/rl_utils.py
index e435c3f5..ca75fd8a 100644
--- a/cmd2/rl_utils.py
+++ b/cmd2/rl_utils.py
@@ -37,8 +37,8 @@ vt100_support = False
# Explanation for why readline wasn't loaded
_rl_warn_reason = ''
-# The order of this check matters since importing pyreadline will also show readline in the modules list
-if 'pyreadline' in sys.modules:
+# The order of this check matters since importing pyreadline/pyreadline3 will also show readline in the modules list
+if 'pyreadline' in sys.modules or 'pyreadline3' in sys.modules:
rl_type = RlType.PYREADLINE
from ctypes import byref
diff --git a/cmd2/table_creator.py b/cmd2/table_creator.py
index 419f12b4..5d6b444d 100644
--- a/cmd2/table_creator.py
+++ b/cmd2/table_creator.py
@@ -83,7 +83,7 @@ class Column:
:param header: label for column header
:param width: display width of column. This does not account for any borders or padding which
may be added (e.g pre_line, inter_cell, and post_line). Header and data text wrap within
- this width using word-based wrapping (defaults to width of header or 1 if header is blank)
+ this width using word-based wrapping (defaults to actual width of header or 1 if header is blank)
:param header_horiz_align: horizontal alignment of header cells (defaults to left)
:param header_vert_align: vertical alignment of header cells (defaults to bottom)
:param data_horiz_align: horizontal alignment of data cells (defaults to left)
@@ -95,12 +95,7 @@ class Column:
"""
self.header = header
- if width is None:
- # Use the width of the widest line in the header or 1 if the header has no width
- line_widths = [ansi.style_aware_wcswidth(line) for line in self.header.splitlines()]
- line_widths.append(1)
- self.width = max(line_widths)
- elif width < 1:
+ if width is not None and width < 1:
raise ValueError("Column width cannot be less than 1")
else:
self.width = width
@@ -137,12 +132,28 @@ class TableCreator:
:param cols: column definitions for this table
:param tab_width: all tabs will be replaced with this many spaces. If a row's fill_char is a tab,
then it will be converted to one space.
+ :raises: ValueError if tab_width is less than 1
"""
+ if tab_width < 1:
+ raise ValueError("Tab width cannot be less than 1")
+
self.cols = copy.copy(cols)
self.tab_width = tab_width
+ for col in self.cols:
+ # Replace tabs before calculating width of header strings
+ col.header = col.header.replace('\t', SPACE * self.tab_width)
+
+ # For headers with the width not yet set, use the width of the
+ # widest line in the header or 1 if the header has no width
+ if col.width is None:
+ line_widths = [ansi.style_aware_wcswidth(line) for line in col.header.splitlines()]
+ line_widths.append(1)
+ col.width = max(line_widths)
+
@staticmethod
- def _wrap_long_word(word: str, max_width: int, max_lines: Union[int, float], is_last_word: bool) -> Tuple[str, int, int]:
+ def _wrap_long_word(word: str, max_width: int, max_lines: Union[int, float],
+ is_last_word: bool) -> Tuple[str, int, int]:
"""
Used by _wrap_text() to wrap a long word over multiple lines
@@ -351,14 +362,16 @@ class TableCreator:
# Stop line loop if we've written to max_lines
if total_lines == max_lines:
- # If this isn't the last data line and there is space left on the final wrapped line, then add an ellipsis
+ # If this isn't the last data line and there is space
+ # left on the final wrapped line, then add an ellipsis
if data_line_index < len(data_str_lines) - 1 and cur_line_width < max_width:
wrapped_buf.write(constants.HORIZONTAL_ELLIPSIS)
break
return wrapped_buf.getvalue()
- def _generate_cell_lines(self, cell_data: Any, is_header: bool, col: Column, fill_char: str) -> Tuple[Deque[str], int]:
+ def _generate_cell_lines(self, cell_data: Any, is_header: bool,
+ col: Column, fill_char: str) -> Tuple[Deque[str], int]:
"""
Generate the lines of a table cell
@@ -398,14 +411,14 @@ class TableCreator:
:param row_data: If this is None then a header row is generated. Otherwise data should have an entry for each
column in the row. (Defaults to None)
- :param fill_char: character that fills remaining space in a cell. Defaults to space. If this is a tab, then it will
- be converted to one space. (Cannot be a line breaking character)
+ :param fill_char: character that fills remaining space in a cell. Defaults to space. If this is a tab,
+ then it will be converted to one space. (Cannot be a line breaking character)
:param pre_line: string to print before each line of a row. This can be used for a left row border and
padding before the first cell's text. (Defaults to blank)
:param inter_cell: string to print where two cells meet. This can be used for a border between cells and padding
between it and the 2 cells' text. (Defaults to 2 spaces)
- :param post_line: string to print after each line of a row. This can be used for padding after the last cell's text
- and a right row border. (Defaults to blank)
+ :param post_line: string to print after each line of a row. This can be used for padding after
+ the last cell's text and a right row border. (Defaults to blank)
:return: row string
:raises: ValueError if data isn't the same length as self.cols
:raises: TypeError if fill_char is more than one character (not including ANSI style sequences)
@@ -608,7 +621,8 @@ class SimpleTable(TableCreator):
:param table_data: Data with an entry for each data row of the table. Each entry should have data for
each column in the row.
:param include_header: If True, then a header will be included at top of table. (Defaults to True)
- :param row_spacing: A number 0 or greater specifying how many blank lines to place between each row (Defaults to 1)
+ :param row_spacing: A number 0 or greater specifying how many blank lines to place between
+ each row (Defaults to 1)
:raises: ValueError if row_spacing is less than 0
"""
if row_spacing < 0:
@@ -820,8 +834,8 @@ class BorderedTable(TableCreator):
class AlternatingTable(BorderedTable):
"""
- Implementation of BorderedTable which uses background colors to distinguish between rows instead of row border lines.
- This class can be used to create the whole table at once or one row at a time.
+ Implementation of BorderedTable which uses background colors to distinguish between rows instead of row border
+ lines. This class can be used to create the whole table at once or one row at a time.
"""
def __init__(self, cols: Sequence[Column], *, tab_width: int = 4, column_borders: bool = True, padding: int = 1,
bg_odd: Optional[ansi.bg] = None, bg_even: Optional[ansi.bg] = ansi.bg.bright_black) -> None:
diff --git a/docs/features/commands.rst b/docs/features/commands.rst
index 8e61a472..cc603f70 100644
--- a/docs/features/commands.rst
+++ b/docs/features/commands.rst
@@ -127,7 +127,7 @@ without errors), and that ``cmd2`` should prompt the user for more input.
If you return ``True`` from a command method, that indicates to ``cmd2`` that
it should stop prompting for user input and cleanly exit. ``cmd2`` already
includes a ``quit`` command, but if you wanted to make another one called
-``finis`` you could::
+``finish`` you could::
def do_finish(self, line):
"""Exit the application"""
@@ -156,7 +156,7 @@ system shell::
"""A simple cmd2 application."""
def do_bail(self, line):
- """Exit the application""
+ """Exit the application"""
self.perror("fatal error, exiting")
self.exit_code = 2
return true
diff --git a/docs/features/modular_commands.rst b/docs/features/modular_commands.rst
index 790b933e..8bd9ba2f 100644
--- a/docs/features/modular_commands.rst
+++ b/docs/features/modular_commands.rst
@@ -29,7 +29,7 @@ Features
See API documentation for :attr:`cmd2.command_definition.CommandSet`
-See the examples for more details: https://github.com/python-cmd2/cmd2/tree/master/plugins/command_sets/examples
+See [the examples](https://github.com/python-cmd2/cmd2/tree/master/examples/modular_commands) for more details.
Defining Commands
diff --git a/docs/features/startup_commands.rst b/docs/features/startup_commands.rst
index aaaf7722..f105054b 100644
--- a/docs/features/startup_commands.rst
+++ b/docs/features/startup_commands.rst
@@ -64,3 +64,12 @@ like so::
This text file should contain a :ref:`Command Script
<features/scripting:Command Scripts>`. See the AliasStartup_ example for a
demonstration.
+
+You can silence a startup script's output by setting ``silent_startup_script``
+to True::
+
+ cmd2.Cmd.__init__(self, startup_script='.cmd2rc', silent_startup_script=True)
+
+Anything written to stderr will still print. Additionally, a startup script
+cannot be silenced if ``allow_redirection`` is False since silencing works
+by redirecting a script's output to ``os.devnull``.
diff --git a/docs/overview/integrating.rst b/docs/overview/integrating.rst
index db5cb206..041083bc 100644
--- a/docs/overview/integrating.rst
+++ b/docs/overview/integrating.rst
@@ -26,10 +26,13 @@ Windows Considerations
If you would like to use :ref:`features/completion:Completion`, and you want
your application to run on Windows, you will need to ensure you install the
-``pyreadline`` package. Make sure to include the following in your
-``setup.py``::
+``pyreadline3`` or ``pyreadline`` package. Make sure to include the following
+in your ``setup.py``::
install_requires=[
'cmd2>=1,<2',
- ":sys_platform=='win32'": ['pyreadline'],
+ ":sys_platform=='win32'": [
+ "pyreadline ; python_version<'3.8'",
+ "pyreadline3 ; python_version>='3.8'", # pyreadline3 is a drop-in replacement for Python 3.8 and above
+ ],
]
diff --git a/setup.py b/setup.py
index 5e2995a9..6b438b13 100755
--- a/setup.py
+++ b/setup.py
@@ -44,8 +44,9 @@ INSTALL_REQUIRES = [
]
EXTRAS_REQUIRE = {
- # Windows also requires pyreadline to ensure tab completion works
- ":sys_platform=='win32'": ['pyreadline'],
+ # Windows also requires pyreadline or the replacement, pyreadline3, to ensure tab completion works
+ ":sys_platform=='win32' and python_version<'3.8'": ["pyreadline"],
+ ":sys_platform=='win32' and python_version>='3.8'": ["pyreadline3"],
# Extra dependencies for running unit tests
'test': [
"gnureadline; sys_platform=='darwin'", # include gnureadline on macOS to ensure it is available in nox env
diff --git a/tests/test_history.py b/tests/test_history.py
index 29ca020a..49898a85 100755
--- a/tests/test_history.py
+++ b/tests/test_history.py
@@ -521,7 +521,7 @@ def test_history_run_one_command(base_app):
out2, err2 = run_cmd(base_app, 'history -r 1')
assert out1 == out2
-def test_history_clear(hist_file):
+def test_history_clear(mocker, hist_file):
# Add commands to history
app = cmd2.Cmd(persistent_history_file=hist_file)
run_cmd(app, 'help')
@@ -539,6 +539,17 @@ def test_history_clear(hist_file):
assert out == []
assert not os.path.exists(hist_file)
+ # Clear the history again and make sure the FileNotFoundError from trying to delete missing history file is silent
+ run_cmd(app, 'history --clear')
+
+ # Cause os.remove to fail and make sure error gets printed
+ mock_remove = mocker.patch('os.remove')
+ mock_remove.side_effect = OSError
+
+ out, err = run_cmd(app, 'history --clear')
+ assert out == []
+ assert 'Error removing history file' in err[0]
+
def test_history_verbose_with_other_options(base_app):
# make sure -v shows a usage error if any other options are present
options_to_test = ['-r', '-e', '-o file', '-t file', '-c', '-x']
diff --git a/tests/test_table_creator.py b/tests/test_table_creator.py
index 58dd6fdf..649674a4 100644
--- a/tests/test_table_creator.py
+++ b/tests/test_table_creator.py
@@ -20,18 +20,6 @@ from cmd2.table_creator import (
def test_column_creation():
- # No width specified, blank label
- c = Column("")
- assert c.width == 1
-
- # No width specified, label isn't blank but has no width
- c = Column(ansi.style('', fg=ansi.fg.green))
- assert c.width == 1
-
- # No width specified, label has width
- c = Column("short\nreally long")
- assert c.width == ansi.style_aware_wcswidth("really long")
-
# Width less than 1
with pytest.raises(ValueError) as excinfo:
Column("Column 1", width=0)
@@ -46,6 +34,36 @@ def test_column_creation():
Column("Column 1", max_data_lines=0)
assert "Max data lines cannot be less than 1" in str(excinfo.value)
+ # No width specified, blank label
+ c = Column("")
+ assert c.width is None
+ tc = TableCreator([c])
+ assert tc.cols[0].width == 1
+
+ # No width specified, label isn't blank but has no width
+ c = Column(ansi.style('', fg=ansi.fg.green))
+ assert c.width is None
+ tc = TableCreator([c])
+ assert tc.cols[0].width == 1
+
+ # No width specified, label has width
+ c = Column("a line")
+ assert c.width is None
+ tc = TableCreator([c])
+ assert tc.cols[0].width == ansi.style_aware_wcswidth("a line")
+
+ # No width specified, label has width and multiple lines
+ c = Column("short\nreally long")
+ assert c.width is None
+ tc = TableCreator([c])
+ assert tc.cols[0].width == ansi.style_aware_wcswidth("really long")
+
+ # No width specified, label has tabs
+ c = Column("line\twith\ttabs")
+ assert c.width is None
+ tc = TableCreator([c])
+ assert tc.cols[0].width == ansi.style_aware_wcswidth("line with tabs")
+
def test_column_alignment():
column_1 = Column("Col 1", width=10,
@@ -241,6 +259,10 @@ def test_tabs():
inter_cell='\t', post_line='\t')
assert row == ' Col 1 Col 2 '
+ with pytest.raises(ValueError) as excinfo:
+ TableCreator([column_1, column_2], tab_width=0)
+ assert "Tab width cannot be less than 1" in str(excinfo.value)
+
def test_simple_table_creation():
column_1 = Column("Col 1", width=16)