Skip to content

Commit

Permalink
Merge branch 'master' into 3.0.0
Browse files Browse the repository at this point in the history
  • Loading branch information
kmvanbrunt committed Nov 28, 2024
2 parents 9f1d162 + abd8bdf commit bfca4e9
Show file tree
Hide file tree
Showing 9 changed files with 298 additions and 155 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
# Set fetch-depth: 0 to fetch all history for all branches and tags.
fetch-depth: 0 # Needed for setuptools_scm to work correctly
- name: Install uv
uses: astral-sh/setup-uv@v3
uses: astral-sh/setup-uv@v4

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@
* Added `RawDescriptionCmd2HelpFormatter`, `RawTextCmd2HelpFormatter`, `ArgumentDefaultsCmd2HelpFormatter`,
and `MetavarTypeCmd2HelpFormatter` and they all use `rich-argparse`.

## 2.5.7 (November 22, 2024)
* Bug Fixes
* Fixed issue where argument parsers for overridden commands were not being created.
* Fixed issue where `Cmd.ppaged()` was not writing to the passed in destination.

## 2.5.6 (November 14, 2024)
* Bug Fixes
* Fixed type hint for `with_default_category` decorator which caused type checkers to mistype
Expand Down
326 changes: 194 additions & 132 deletions cmd2/cmd2.py

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion cmd2/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,9 @@ def cmd_wrapper(*args: Any, **kwargs: dict[str, Any]) -> Optional[bool]:
statement, parsed_arglist = cmd2_app.statement_parser.get_command_arg_list(
command_name, statement_arg, preserve_quotes
)
arg_parser = cmd2_app._command_parsers.get(command_name, None)

# Pass cmd_wrapper instead of func, since it contains the parser info.
arg_parser = cmd2_app._command_parsers.get(cmd_wrapper)
if arg_parser is None:
# This shouldn't be possible to reach
raise ValueError(f'No argument parser found for {command_name}') # pragma: no cover
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,7 @@ dev-dependencies = [
"pytest",
"pytest-cov",
"pytest-mock",
"ruff",
"sphinx",
"sphinx-autobuild",
"sphinx-rtd-theme",
Expand Down
4 changes: 1 addition & 3 deletions tests/test_argparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
import cmd2

from .conftest import (
find_subcommand,
run_cmd,
)

Expand Down Expand Up @@ -386,8 +385,7 @@ def test_add_another_subcommand(subcommand_app):
This tests makes sure _set_parser_prog() sets _prog_prefix on every _SubParsersAction so that all future calls
to add_parser() write the correct prog value to the parser being added.
"""
base_parser = subcommand_app._command_parsers.get('base')
find_subcommand(subcommand_app._command_parsers.get('base'), [])
base_parser = subcommand_app._command_parsers.get(subcommand_app.do_base)
for sub_action in base_parser._actions:
if isinstance(sub_action, argparse._SubParsersAction):
new_parser = sub_action.add_parser('new_sub', help='stuff')
Expand Down
49 changes: 35 additions & 14 deletions tests/test_cmd2.py
Original file line number Diff line number Diff line change
Expand Up @@ -1210,6 +1210,13 @@ def do_multiline_docstr(self, arg):
"""
pass

parser_cmd_parser = cmd2.Cmd2ArgumentParser(description="This is the description.")

@cmd2.with_argparser(parser_cmd_parser)
def do_parser_cmd(self, args):
"""This is the docstring."""
pass


@pytest.fixture
def help_app():
Expand Down Expand Up @@ -1249,6 +1256,11 @@ def test_help_multiline_docstring(help_app):
assert help_app.last_result is True


def test_help_verbose_uses_parser_description(help_app: HelpApp):
out, err = run_cmd(help_app, 'help --verbose')
verify_help_text(help_app, out, verbose_strings=[help_app.parser_cmd_parser.description])


class HelpCategoriesApp(cmd2.Cmd):
"""Class for testing custom help_* methods which override docstring help."""

Expand Down Expand Up @@ -2224,20 +2236,6 @@ def test_ppaged(outsim_app):
assert out == msg + end


def test_ppaged_blank(outsim_app):
msg = ''
outsim_app.ppaged(msg)
out = outsim_app.stdout.getvalue()
assert not out


def test_ppaged_none(outsim_app):
msg = None
outsim_app.ppaged(msg)
out = outsim_app.stdout.getvalue()
assert not out


@with_ansi_style(ansi.AllowStyle.TERMINAL)
def test_ppaged_strips_ansi_when_redirecting(outsim_app):
msg = 'testing...'
Expand Down Expand Up @@ -2771,3 +2769,26 @@ def test_columnize_too_wide(outsim_app):

expected = "\n".join(str_list) + "\n"
assert outsim_app.stdout.getvalue() == expected


def test_command_parser_retrieval(outsim_app: cmd2.Cmd):
# Pass something that isn't a method
not_a_method = "just a string"
assert outsim_app._command_parsers.get(not_a_method) is None

# Pass a non-command method
assert outsim_app._command_parsers.get(outsim_app.__init__) is None


def test_command_synonym_parser():
# Make sure a command synonym returns the same parser as what it aliases
class SynonymApp(cmd2.cmd2.Cmd):
do_synonym = cmd2.cmd2.Cmd.do_help

app = SynonymApp()

synonym_parser = app._command_parsers.get(app.do_synonym)
help_parser = app._command_parsers.get(app.do_help)

assert synonym_parser is not None
assert synonym_parser is help_parser
2 changes: 1 addition & 1 deletion tests/transcripts/from_cmdloop.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# so you can see where they are.

(Cmd) help say
Usage: say [-h] [-p] [-s] [-r REPEAT]/ */
Usage: speak [-h] [-p] [-s] [-r REPEAT]/ */

Repeats what you tell me to./ */

Expand Down
60 changes: 57 additions & 3 deletions tests_isolated/test_commandset/test_commandset.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,52 @@ def test_autoload_commands(command_sets_app):
assert 'Command Set B' not in cmds_cats


def test_command_synonyms():
"""Test the use of command synonyms in CommandSets"""

class SynonymCommandSet(cmd2.CommandSet):
def __init__(self, arg1):
super().__init__()
self._arg1 = arg1

@cmd2.with_argparser(cmd2.Cmd2ArgumentParser(description="Native Command"))
def do_builtin(self, _):
pass

# Create a synonym to a command inside of this CommandSet
do_builtin_synonym = do_builtin

# Create a synonym to a command outside of this CommandSet with subcommands.
# This will best test the synonym check in cmd2.Cmd._check_uninstallable() when
# we unresgister this CommandSet.
do_alias_synonym = cmd2.Cmd.do_alias

cs = SynonymCommandSet("foo")
app = WithCommandSets(command_sets=[cs])

# Make sure the synonyms have the same parser as what they alias
builtin_parser = app._command_parsers.get(app.do_builtin)
builtin_synonym_parser = app._command_parsers.get(app.do_builtin_synonym)
assert builtin_parser is not None
assert builtin_parser is builtin_synonym_parser

alias_parser = app._command_parsers.get(cmd2.Cmd.do_alias)
alias_synonym_parser = app._command_parsers.get(app.do_alias_synonym)
assert alias_parser is not None
assert alias_parser is alias_synonym_parser

# Unregister the CommandSet and make sure built-in command and synonyms are gone
app.unregister_command_set(cs)
assert not hasattr(app, "do_builtin")
assert not hasattr(app, "do_builtin_synonym")
assert not hasattr(app, "do_alias_synonym")

# Make sure the alias command still exists, has the same parser, and works.
assert alias_parser is app._command_parsers.get(cmd2.Cmd.do_alias)
out, err = run_cmd(app, 'alias --help')
assert normalize(alias_parser.format_help())[0] in out


def test_custom_construct_commandsets():
command_set_b = CommandSetB('foo')

Expand Down Expand Up @@ -288,7 +334,7 @@ def test_load_commandset_errors(command_sets_manual, capsys):
cmd_set = CommandSetA()

# create a conflicting command before installing CommandSet to verify rollback behavior
command_sets_manual._install_command_function('durian', cmd_set.do_durian)
command_sets_manual._install_command_function('do_durian', cmd_set.do_durian)
with pytest.raises(CommandSetRegistrationError):
command_sets_manual.register_command_set(cmd_set)

Expand All @@ -313,13 +359,21 @@ def test_load_commandset_errors(command_sets_manual, capsys):
assert "Deleting alias 'apple'" in err
assert "Deleting alias 'banana'" in err

# verify command functions which don't start with "do_" raise an exception
with pytest.raises(CommandSetRegistrationError):
command_sets_manual._install_command_function('new_cmd', cmd_set.do_banana)

# verify methods which don't start with "do_" raise an exception
with pytest.raises(CommandSetRegistrationError):
command_sets_manual._install_command_function('do_new_cmd', cmd_set.on_register)

# verify duplicate commands are detected
with pytest.raises(CommandSetRegistrationError):
command_sets_manual._install_command_function('banana', cmd_set.do_banana)
command_sets_manual._install_command_function('do_banana', cmd_set.do_banana)

# verify bad command names are detected
with pytest.raises(CommandSetRegistrationError):
command_sets_manual._install_command_function('bad command', cmd_set.do_banana)
command_sets_manual._install_command_function('do_bad command', cmd_set.do_banana)

# verify error conflict with existing completer function
with pytest.raises(CommandSetRegistrationError):
Expand Down

0 comments on commit bfca4e9

Please sign in to comment.