diff options
| author | Kevin Van Brunt <kmvanbrunt@gmail.com> | 2021-01-29 20:07:46 -0500 |
|---|---|---|
| committer | Kevin Van Brunt <kmvanbrunt@gmail.com> | 2021-01-29 20:07:46 -0500 |
| commit | 434a01f44e7d2302b4deef8f2e8069cbc26df560 (patch) | |
| tree | cb3d5425668954fdd92989121ab53741c56908f8 | |
| parent | 82fe1d473ca2fe0278568036659fa78cb3c17f78 (diff) | |
| parent | cd377071cd122bc92a829322e00ae43fd5a5c254 (diff) | |
| download | cmd2-git-434a01f44e7d2302b4deef8f2e8069cbc26df560.tar.gz | |
Merge branch 'master' into 2.0
| -rw-r--r-- | CHANGELOG.md | 3 | ||||
| -rw-r--r-- | Pipfile | 3 | ||||
| -rw-r--r-- | cmd2/cmd2.py | 8 | ||||
| -rw-r--r-- | cmd2/rl_utils.py | 4 | ||||
| -rw-r--r-- | cmd2/table_creator.py | 48 | ||||
| -rw-r--r-- | docs/features/commands.rst | 4 | ||||
| -rw-r--r-- | docs/features/modular_commands.rst | 2 | ||||
| -rw-r--r-- | docs/features/startup_commands.rst | 9 | ||||
| -rw-r--r-- | docs/overview/integrating.rst | 9 | ||||
| -rwxr-xr-x | setup.py | 5 | ||||
| -rwxr-xr-x | tests/test_history.py | 13 | ||||
| -rw-r--r-- | tests/test_table_creator.py | 46 |
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 @@ -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 + ], ] @@ -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) |
