Repository: python-cmd2/cmd2

Issue:
CMD2 gives Error 'width must be at least 1' When using Columnize to order a list of items with long names
**Issue:**
The strings are too long to make a table with the values and fails with the error `EXCEPTION of type 'ValueError' occurred with message: width must be at least 1`

**Additional details:**
This occurs during the columnize function that counts the characters of multiple strings at once and makes sure that it doesn't go over the width of the screen. (set as 79 characters) 

If it does go over the width it sets the column width as 0. That 0 is passed into the align_left() then into align_text() function that then fails with the error that the width is 0.

https://github.com/python-cmd2/cmd2/blob/e95af81acb9c6829c969f76e40658356a4808edb/cmd2/cmd2.py#L3693

The problem is that the width of the strings of the list values goes over that 79 when placed side by side, but instead of showing it in a straight list it fails since the column width was set to 0. 

Changing the `0` to `None` and it is then it is able to load the list with no issues as one column. 

**Full traceback:**
```
Traceback (most recent call last):
  File "/home/kassandra/builder/virtualenv/lib/python3.9/site-packages/cmd2/cmd2.py", line 2398, in onecmd_plus_hooks
    stop = self.onecmd(statement, add_to_history=add_to_history)
  File "/home/kassandra/builder/virtualenv/lib/python3.9/site-packages/cmd2/cmd2.py", line 2849, in onecmd
    stop = func(statement)
  File "/home/kassandra/builder/virtualenv/lib/python3.9/site-packages/cmd2/decorators.py", line 379, in cmd_wrapper
    return func(*args_list, **kwargs)  # type: ignore[call-arg]
  File "/home/kassandra/builder/virtualenv/lib/python3.9/site-packages/cmd2/cmd2.py", line 3603, in do_help
    self._help_menu(args.verbose)
  File "/home/kassandra/builder/virtualenv/lib/python3.9/site-packages/cmd2/cmd2.py", line 3721, in _help_menu
    self._print_topics(category, cmds_cats[category], verbose)
  File "/home/kassandra/builder/virtualenv/lib/python3.9/site-packages/cmd2/cmd2.py", line 3763, in _print_topics
    self.print_topics(header, cmds, 15, 80)
  File "/home/kassandra/builder/virtualenv/lib/python3.9/site-packages/cmd2/cmd2.py", line 3649, in print_topics
    self.columnize(cmds, maxcol - 1)
  File "/home/kassandra/builder/virtualenv/lib/python3.9/site-packages/cmd2/cmd2.py", line 3705, in columnize
    texts[col] = utils.align_left(texts[col], width=colwidths[col])
  File "/home/kassandra/builder/virtualenv/lib/python3.9/site-packages/cmd2/utils.py", line 981, in align_left
    return align_text(text, TextAlignment.LEFT, fill_char=fill_char, width=width, tab_width=tab_width, truncate=truncate)
  File "/home/kassandra/builder/virtualenv/lib/python3.9/site-packages/cmd2/utils.py", line 869, in align_text
    raise ValueError("width must be at least 1")
ValueError: width must be at least 1
EXCEPTION of type 'ValueError' occurred with message: width must be at least 1
```

Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/python-cmd2/cmd2/pull/1256?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-cmd2) Report
Base: **98.52**% // Head: **97.96**% // Decreases project coverage by **`-0.56%`** :warning:
> Coverage data is based on head [(`2fa4622`)](https://codecov.io/gh/python-cmd2/cmd2/pull/1256?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-cmd2) compared to base [(`23a7d22`)](https://codecov.io/gh/python-cmd2/cmd2/commit/23a7d22cb3d2261e77bed3480f33f45cafd21cc2?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-cmd2).
> Patch coverage: 100.00% of modified lines in pull request are covered.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master    #1256      +/-   ##
==========================================
- Coverage   98.52%   97.96%   -0.56%     
==========================================
  Files          21       22       +1     
  Lines        5677     5703      +26     
==========================================
- Hits         5593     5587       -6     
- Misses         84      116      +32     
```


| [Impacted Files](https://codecov.io/gh/python-cmd2/cmd2/pull/1256?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-cmd2) | Coverage Δ | |
|---|---|---|
| [cmd2/cmd2.py](https://codecov.io/gh/python-cmd2/cmd2/pull/1256?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-cmd2#diff-Y21kMi9jbWQyLnB5) | `97.63% <100.00%> (-0.57%)` | :arrow_down: |
| [cmd2/rl\_utils.py](https://codecov.io/gh/python-cmd2/cmd2/pull/1256?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-cmd2#diff-Y21kMi9ybF91dGlscy5weQ==) | `71.87% <0.00%> (-25.00%)` | :arrow_down: |
| [cmd2/utils.py](https://codecov.io/gh/python-cmd2/cmd2/pull/1256?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-cmd2#diff-Y21kMi91dGlscy5weQ==) | `97.41% <0.00%> (-0.60%)` | :arrow_down: |
| [cmd2/\_\_init\_\_.py](https://codecov.io/gh/python-cmd2/cmd2/pull/1256?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-cmd2#diff-Y21kMi9fX2luaXRfXy5weQ==) | `100.00% <0.00%> (ø)` | |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-cmd2). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-cmd2)

</details>

[:umbrella: View full report at Codecov](https://codecov.io/gh/python-cmd2/cmd2/pull/1256?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-cmd2).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-cmd2).

tleonhardt: @kmvanbrunt Need to fix failing unit tests I think.

Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 92ac77dd..4f8bac8a 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -9,7 +9,7 @@ jobs:
     strategy:
       matrix:
         os: [ubuntu-latest, macos-latest, windows-latest]
-        python-version: ["3.6", "3.7", "3.8", "3.9", "3.10"]
+        python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
       fail-fast: false
     runs-on: ${{ matrix.os }}
     steps:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b972fc82..ef4b6883 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,10 @@
+## 2.4.3 (TBD, 2023)
+* Bug Fixes
+  * Fixed ValueError caused when passing `Cmd.columnize()` strings wider than `display_width`.
+* Enhancements
+  * Renamed `utils.str_to_bool()` -> `utils.to_bool()`.
+  * Enhanced `utils.to_bool()` so that it accepts and converts `bool`, `int`, and `float` in addition to `str`.
+
 ## 2.4.2 (July 13, 2022)
 * Enhancements
   * Updated argparse decorator to remove annotations when the docstring is used for a command's help text.
diff --git a/cmd2/argparse_custom.py b/cmd2/argparse_custom.py
index fea6d9cd..a8ad1ebd 100644
--- a/cmd2/argparse_custom.py
+++ b/cmd2/argparse_custom.py
@@ -276,7 +276,7 @@ try:
         runtime_checkable,
     )
 except ImportError:
-    from typing_extensions import (  # type: ignore[misc]
+    from typing_extensions import (  # type: ignore[assignment]
         Protocol,
         runtime_checkable,
     )
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py
index 7fb85a1a..6cd8b950 100644
--- a/cmd2/cmd2.py
+++ b/cmd2/cmd2.py
@@ -2431,7 +2431,8 @@ class Cmd(cmd.Cmd):
             if raise_keyboard_interrupt and not stop:
                 raise ex
         except SystemExit as ex:
-            self.exit_code = ex.code
+            if isinstance(ex.code, int):
+                self.exit_code = ex.code
             stop = True
         except PassThroughException as ex:
             raise ex.wrapped_ex
@@ -2444,7 +2445,8 @@ class Cmd(cmd.Cmd):
                 if raise_keyboard_interrupt and not stop:
                     raise ex
             except SystemExit as ex:
-                self.exit_code = ex.code
+                if isinstance(ex.code, int):
+                    self.exit_code = ex.code
                 stop = True
             except PassThroughException as ex:
                 raise ex.wrapped_ex
@@ -3688,9 +3690,10 @@ class Cmd(cmd.Cmd):
             if totwidth <= display_width:
                 break
         else:
+            # The output is wider than display_width. Print 1 column with each string on its own row.
             nrows = len(str_list)
             ncols = 1
-            colwidths = [0]
+            colwidths = [1]
         for row in range(nrows):
             texts = []
             for col in range(ncols):
diff --git a/noxfile.py b/noxfile.py
index 427a7c3f..14b4d5a0 100644
--- a/noxfile.py
+++ b/noxfile.py
@@ -17,7 +17,7 @@ def docs(session):
     )
 
 
-@nox.session(python=['3.6', '3.7', '3.8', '3.9', '3.10'])
+@nox.session(python=['3.7', '3.8', '3.9', '3.10', '3.11'])
 @nox.parametrize('plugin', [None, 'ext_test', 'template', 'coverage'])
 def tests(session, plugin):
     if plugin is None:
diff --git a/plugins/template/README.md b/plugins/template/README.md
index 904b0541..509fa0c0 100644
--- a/plugins/template/README.md
+++ b/plugins/template/README.md
@@ -298,8 +298,9 @@ unit tests found in the `tests` directory.
 
 ### Use nox to run unit tests in multiple versions of python
 
-The included `noxfile.py` is setup to run the unit tests in python 3.6, 3.7, 3.8,
-and 3.9. You can run your unit tests in all of these versions of python by:
+The included `noxfile.py` is setup to run the unit tests in python 3.7, 3.8,
+3.9, 3.10, and 3.11 You can run your unit tests in all of these versions of
+python by:
 ```
 $ nox
 ```
diff --git a/plugins/template/build-pyenvs.sh b/plugins/template/build-pyenvs.sh
index d64e11bd..f4084494 100644
--- a/plugins/template/build-pyenvs.sh
+++ b/plugins/template/build-pyenvs.sh
@@ -23,7 +23,7 @@
 # virtualenvs will be added to '.python-version'. Feel free to modify
 # this list, but note that this script intentionally won't install
 # dev, rc, or beta python releases
-declare -a pythons=("3.7" "3.6" "3.8" "3.9")
+declare -a pythons=("3.7" "3.8" "3.9" "3.10" "3.11")
 
 # function to find the latest patch of a minor version of python
 function find_latest_version {
diff --git a/plugins/template/noxfile.py b/plugins/template/noxfile.py
index 75eab841..9872e193 100644
--- a/plugins/template/noxfile.py
+++ b/plugins/template/noxfile.py
@@ -1,7 +1,7 @@
 import nox
 
 
-@nox.session(python=['3.6', '3.7', '3.8', '3.9'])
+@nox.session(python=['3.7', '3.8', '3.9', '3.10', '3.11'])
 def tests(session):
     session.install('invoke', './[test]')
     session.run('invoke', 'pytest', '--junit', '--no-pty')
diff --git a/plugins/template/setup.py b/plugins/template/setup.py
index 18ab88cf..5fab9f06 100644
--- a/plugins/template/setup.py
+++ b/plugins/template/setup.py
@@ -39,6 +39,7 @@ setuptools.setup(
         'Programming Language :: Python :: 3.8',
         'Programming Language :: Python :: 3.9',
         'Programming Language :: Python :: 3.10',
+        'Programming Language :: Python :: 3.11',
     ],
     # dependencies for development and testing
     # $ pip install -e .[dev]
diff --git a/setup.cfg b/setup.cfg
index d2ca017b..5d42015c 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -9,7 +9,7 @@ addopts =
 
 [flake8]
 count = True
-ignore = E203,E231,W503  # E231 can be removed once black is fixed.
+ignore = E203,W503
 max-complexity = 26
 max-line-length = 127
 show-source = True


Test patch:
diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py
index 4a29c2f6..5bcceb34 100755
--- a/tests/test_cmd2.py
+++ b/tests/test_cmd2.py
@@ -1454,7 +1454,7 @@ def test_select_eof(select_app, monkeypatch):
     assert read_input_mock.call_count == 2
 
 
-def test_select_ctrl_c(outsim_app, monkeypatch, capsys):
+def test_select_ctrl_c(outsim_app, monkeypatch):
     # Ctrl-C during select prints ^C and raises a KeyboardInterrupt
     read_input_mock = mock.MagicMock(name='read_input', side_effect=KeyboardInterrupt)
     monkeypatch.setattr("cmd2.Cmd.read_input", read_input_mock)
@@ -2866,3 +2866,12 @@ def test_transcripts_at_init():
     transcript_files = ['foo', 'bar']
     app = cmd2.Cmd(allow_cli_args=False, transcript_files=transcript_files)
     assert app._transcript_files == transcript_files
+
+
+def test_columnize_too_wide(outsim_app):
+    """Test calling columnize with output that wider than display_width"""
+    str_list = ["way too wide", "much wider than the first"]
+    outsim_app.columnize(str_list, display_width=5)
+
+    expected = "\n".join(str_list) + "\n"
+    assert outsim_app.stdout.getvalue() == expected