neithere / argh

An argparse wrapper that doesn't make you say "argh" each time you deal with it.
http://argh.rtfd.org
GNU Lesser General Public License v3.0
369 stars 55 forks source link

Test failures with Python 3.13.0b2 #228

Closed mgorny closed 2 months ago

mgorny commented 3 months ago

Summary

The test suite fails with Python 3.13.0b2. At a first glance, all the mismatches are because --help output changed. When a single argument has multiple option strings, argparse no longer repeats the placeholder, i.e. rather than -t TASK, --task TASK it gives -t, --task TASK.

To Reproduce

Command line input/output:

$ tox -e py313
.pkg: install_requires> python -I -m pip install 'flit_core<4,>=3.2'
.pkg: _optional_hooks> python /usr/lib/python3.12/site-packages/pyproject_api/_backend.py True flit_core.buildapi
.pkg: get_requires_for_build_sdist> python /usr/lib/python3.12/site-packages/pyproject_api/_backend.py True flit_core.buildapi
.pkg: build_sdist> python /usr/lib/python3.12/site-packages/pyproject_api/_backend.py True flit_core.buildapi
py313: install_package_deps> python -I -m pip install 'pytest-cov>=4.1' 'pytest>=7.4' 'tox>=4.11.3'
py313: install_package> python -I -m pip install --force-reinstall --no-deps /tmp/argh/.tox/.tmp/package/1/argh-0.31.2.tar.gz
py313: commands[0]> pytest --cov=argh --cov-report html --cov-fail-under 100 tests
========================================================= test session starts =========================================================
platform linux -- Python 3.13.0b2, pytest-8.2.2, pluggy-1.5.0
cachedir: .tox/py313/.pytest_cache
rootdir: /tmp/argh
configfile: pyproject.toml
plugins: cov-5.0.0
collected 169 items                                                                                                                   

tests/test_assembling.py ......................................                                                                 [ 22%]
tests/test_completion.py ...                                                                                                    [ 24%]
tests/test_decorators.py ......                                                                                                 [ 27%]
tests/test_dispatching.py ........                                                                                              [ 32%]
tests/test_dto.py ....                                                                                                          [ 34%]
tests/test_integration.py ........................................FF.....F..F.F..                                               [ 67%]
tests/test_interaction.py .....                                                                                                 [ 70%]
tests/test_mapping_policies.py ..........................                                                                       [ 85%]
tests/test_regressions.py ............                                                                                          [ 92%]
tests/test_typing_hints.py ........                                                                                             [ 97%]
tests/test_utils.py ....                                                                                                        [100%]

============================================================== FAILURES ===============================================================
___________________________________________________ test_default_arg_values_in_help ___________________________________________________

    def test_default_arg_values_in_help():
        "Argument defaults should appear in the help message implicitly"

        @argh.arg("name", default="Basil")
        @argh.arg("--task", default="hang the Moose")
        @argh.arg("--note", help="why is it a remarkable animal?")
        def remind(
            name,
            *,
            task=None,
            reason="there are creatures living in it",
            note="it can speak English",
        ):
            return "Oh what is it now, can't you leave me in peace..."

        parser = DebugArghParser()
        parser.set_default_command(remind)

        help_normalised = re.sub(r"\s+", " ", parser.format_help())

        assert "name 'Basil'" in help_normalised
>       assert "-t TASK, --task TASK 'hang the Moose'" in help_normalised
E       assert "-t TASK, --task TASK 'hang the Moose'" in "usage: pytest [-h] [-t TASK] [-r REASON] [-n NOTE] name positional arguments: name 'Basil' options: -h, --help show t...N 'there are creatures living in it' -n, --note NOTE why is it a remarkable animal? (default: 'it can speak English') "

tests/test_integration.py:727: AssertionError
_____________________________________________ test_default_arg_values_in_help__regression _____________________________________________

    def test_default_arg_values_in_help__regression():
        "Empty string as default value → empty help string → broken argparse"

        def foo(*, bar=""):
            return bar

        parser = DebugArghParser()
        parser.set_default_command(foo)

        # doesn't break
        parser.format_help()

        # now check details
>       assert "-b BAR, --bar BAR  ''" in parser.format_help()
E       assert "-b BAR, --bar BAR  ''" in "usage: pytest [-h] [-b BAR]\n\noptions:\n  -h, --help     show this help message and exit\n  -b, --bar BAR  ''\n"
E        +  where "usage: pytest [-h] [-b BAR]\n\noptions:\n  -h, --help     show this help message and exit\n  -b, --bar BAR  ''\n" = <bound method ArgumentParser.format_help of DebugArghParser(prog='pytest', usage=None, description=None, formatter_class=<class 'argh.constants.CustomFormatter'>, conflict_handler='error', add_help=True)>()
E        +    where <bound method ArgumentParser.format_help of DebugArghParser(prog='pytest', usage=None, description=None, formatter_class=<class 'argh.constants.CustomFormatter'>, conflict_handler='error', add_help=True)> = DebugArghParser(prog='pytest', usage=None, description=None, formatter_class=<class 'argh.constants.CustomFormatter'>, conflict_handler='error', add_help=True).format_help

tests/test_integration.py:754: AssertionError
___________________________________________________ test_add_commands_no_overrides2 ___________________________________________________

capsys = <_pytest.capture.CaptureFixture object at 0x7f319ffd4190>

    def test_add_commands_no_overrides2(capsys: pytest.CaptureFixture[str]):
        def first_func(*, foo=123):
            """Owl stretching time"""

        def second_func():
            pass

        parser = argh.ArghParser(prog="myapp")
        parser.add_commands([first_func, second_func])

        run(parser, "first-func --help", exit=True)
        captured = capsys.readouterr()
>       assert (
            captured.out
            == unindent(
                f"""
            usage: myapp first-func [-h] [-f FOO]

            Owl stretching time

            {HELP_OPTIONS_LABEL}:
              -h, --help         show this help message and exit
              -f FOO, --foo FOO  123
            """
            )[1:]
        )
E       AssertionError: assert 'usage: myapp...oo FOO  123\n' == 'usage: myapp...oo FOO  123\n'
E         
E         Skipping 76 identical leading characters in diff, use -v to show
E         - -help         show this help message and exit
E         ?           ----
E         + -help     show this help message and exit
E         -   -f FOO, --foo FOO  123
E         ?     ----
E         +   -f, --foo FOO  123

tests/test_integration.py:871: AssertionError
_________________________________________________ test_add_commands_group_overrides3 __________________________________________________

capsys = <_pytest.capture.CaptureFixture object at 0x7f31a0056210>

    def test_add_commands_group_overrides3(capsys: pytest.CaptureFixture[str]):
        """
        When `group_kwargs` is passed to `add_commands()`, its members override
        whatever was specified on function level.
        """

        def first_func(*, foo=123):
            """Owl stretching time"""
            return foo

        def second_func():
            pass

        parser = argh.ArghParser(prog="myapp")
        parser.add_commands(
            [first_func, second_func],
            group_name="my-group",
            group_kwargs={
                "help": "group help override",
                "description": "group description override",
            },
        )

        run(parser, "my-group first-func --help", exit=True)
        captured = capsys.readouterr()
>       assert (
            captured.out
            == unindent(
                f"""
            usage: myapp my-group first-func [-h] [-f FOO]

            Owl stretching time

            {HELP_OPTIONS_LABEL}:
              -h, --help         show this help message and exit
              -f FOO, --foo FOO  123
            """
            )[1:]
        )
E       AssertionError: assert 'usage: myapp...oo FOO  123\n' == 'usage: myapp...oo FOO  123\n'
E         
E         Skipping 85 identical leading characters in diff, use -v to show
E         - -help         show this help message and exit
E         ?           ----
E         + -help     show this help message and exit
E         -   -f FOO, --foo FOO  123
E         ?     ----
E         +   -f, --foo FOO  123

tests/test_integration.py:1000: AssertionError
__________________________________________________ test_add_commands_func_overrides2 __________________________________________________

capsys = <_pytest.capture.CaptureFixture object at 0x7f31a0013460>

    def test_add_commands_func_overrides2(capsys: pytest.CaptureFixture[str]):
        """
        When `func_kwargs` is passed to `add_commands()`, its members override
        whatever was specified on function level.
        """

        def first_func(*, foo=123):
            """Owl stretching time"""
            pass

        def second_func():
            pass

        parser = argh.ArghParser(prog="myapp")
        parser.add_commands(
            [first_func, second_func],
            func_kwargs={
                "help": "func help override",
                "description": "func description override",
            },
        )

        run(parser, "first-func --help", exit=True)
        captured = capsys.readouterr()
>       assert (
            captured.out
            == unindent(
                f"""
            usage: myapp first-func [-h] [-f FOO]

            func description override

            {HELP_OPTIONS_LABEL}:
              -h, --help         show this help message and exit
              -f FOO, --foo FOO  123
            """
            )[1:]
        )
E       AssertionError: assert 'usage: myapp...oo FOO  123\n' == 'usage: myapp...oo FOO  123\n'
E         
E         Skipping 82 identical leading characters in diff, use -v to show
E         - -help         show this help message and exit
E         ?           ----
E         + -help     show this help message and exit
E         -   -f FOO, --foo FOO  123
E         ?     ----
E         +   -f, --foo FOO  123

tests/test_integration.py:1082: AssertionError
========================================================== warnings summary ===========================================================
tests/test_dispatching.py::test_dispatch_command_two_stage
  /tmp/argh/src/argh/dispatching.py:167: DeprecationWarning: The argument `namespace` in `dispatch()` is deprecated. It will be removed in the next minor version after v0.31.
    warnings.warn(

-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html

----------- coverage: platform linux, python 3.13.0-beta-2 -----------
Coverage HTML written to dir htmlcov

Required test coverage of 100% reached. Total coverage: 100.00%
======================================================= short test summary info =======================================================
FAILED tests/test_integration.py::test_default_arg_values_in_help - assert "-t TASK, --task TASK 'hang the Moose'" in "usage: pytest [-h] [-t TASK] [-r REASON] [-n NOTE] name positional arguments: n...
FAILED tests/test_integration.py::test_default_arg_values_in_help__regression - assert "-b BAR, --bar BAR  ''" in "usage: pytest [-h] [-b BAR]\n\noptions:\n  -h, --help     show this help message and exit\n  -b...
FAILED tests/test_integration.py::test_add_commands_no_overrides2 - AssertionError: assert 'usage: myapp...oo FOO  123\n' == 'usage: myapp...oo FOO  123\n'
FAILED tests/test_integration.py::test_add_commands_group_overrides3 - AssertionError: assert 'usage: myapp...oo FOO  123\n' == 'usage: myapp...oo FOO  123\n'
FAILED tests/test_integration.py::test_add_commands_func_overrides2 - AssertionError: assert 'usage: myapp...oo FOO  123\n' == 'usage: myapp...oo FOO  123\n'
============================================== 5 failed, 164 passed, 1 warning in 2.72s ===============================================
py313: exit 1 (3.22 seconds) /tmp/argh> pytest --cov=argh --cov-report html --cov-fail-under 100 tests pid=116532
.pkg: _exit> python /usr/lib/python3.12/site-packages/pyproject_api/_backend.py True flit_core.buildapi
  py313: FAIL code 1 (13.82=setup[10.60]+cmd[3.22] seconds)
  evaluation failed :( (13.95 seconds)

Expected behavior

Passing tests :-).

Environment

Additional context

n/a

neithere commented 3 months ago

Thanks you for the report, @mgorny!

Looks like it's caused by this one: https://github.com/python/cpython/pull/103372

Will update the tests.

neithere commented 3 months ago

Will be released with https://github.com/neithere/argh/pull/225.

mgorny commented 2 months ago

Thanks. I can confirm that the tests pass for with this commit applied.