From 1977630c33632e59a1b3285ebbe5acaa65b7a813 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Mon, 30 Sep 2024 19:59:36 +0300 Subject: [PATCH 1/9] gh-66436: Improved prog default value for argparse.ArgumentParser It can now have one of three forms: * basename(argv0) -- for simple scripts * python arv0 -- for directories, ZIP files, etc * python -m module -- for imported modules --- Doc/library/argparse.rst | 40 +++++-- Doc/whatsnew/3.14.rst | 7 ++ Lib/argparse.py | 20 +++- Lib/test/support/import_helper.py | 2 +- Lib/test/test_argparse.py | 104 +++++++++++++++++- ...4-09-30-19-59-28.gh-issue-66436.4gYN_n.rst | 4 + 6 files changed, 160 insertions(+), 17 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2024-09-30-19-59-28.gh-issue-66436.4gYN_n.rst diff --git a/Doc/library/argparse.rst b/Doc/library/argparse.rst index a4683bccf651cd..32cea131415658 100644 --- a/Doc/library/argparse.rst +++ b/Doc/library/argparse.rst @@ -30,7 +30,7 @@ Quick Links for ArgumentParser ========================= =========================================================================================================== ================================================================================== Name Description Values ========================= =========================================================================================================== ================================================================================== -prog_ The name of the program Defaults to ``os.path.basename(sys.argv[0])`` +prog_ The name of the program usage_ The string describing the program usage description_ A brief description of what the program does epilog_ Additional description of the program after the argument help @@ -214,8 +214,8 @@ ArgumentParser objects as keyword arguments. Each parameter has its own more detailed description below, but in short they are: - * prog_ - The name of the program (default: - ``os.path.basename(sys.argv[0])``) + * prog_ - The name of the program (default: generated from the ``__main__`` + module attributes and ``sys.argv[0]``) * usage_ - The string describing the program usage (default: generated from arguments added to parser) @@ -268,10 +268,18 @@ The following sections describe how each of these are used. prog ^^^^ -By default, :class:`ArgumentParser` objects use the base name -(see :func:`os.path.basename`) of ``sys.argv[0]`` to determine -how to display the name of the program in help messages. This default is almost -always desirable because it will make the help messages match the name that was +By default, :class:`ArgumentParser` calculates the name of the program +to display in help messages depending on the way the Python inerpreter was run: + +* The :func:`base name ` of ``sys.argv[0]`` if a file was + passed as argument. +* The Python inerpreter name followed by ``sys.argv[0]`` if a directory or + a zipfile was passed as argument. +* The Python inerpreter name followed by ``-m`` followed by the + module or package name if the :option:`-m` option was used. + +This default is almost +always desirable because it will make the help messages match the string that was used to invoke the program on the command line. For example, consider a file named ``myprogram.py`` with the following code:: @@ -281,7 +289,7 @@ named ``myprogram.py`` with the following code:: args = parser.parse_args() The help for this program will display ``myprogram.py`` as the program name -(regardless of where the program was invoked from): +(regardless of where the program was invoked from) if run it as a script: .. code-block:: shell-session @@ -299,6 +307,17 @@ The help for this program will display ``myprogram.py`` as the program name -h, --help show this help message and exit --foo FOO foo help +If import it as a module, the help will display a corresponding command line: + +.. code-block:: shell-session + + $ /usr/bin/python3 -m subdir.myprogram --help + usage: python3 -m subdir.myprogram [-h] [--foo FOO] + + options: + -h, --help show this help message and exit + --foo FOO foo help + To change this default behavior, another value can be supplied using the ``prog=`` argument to :class:`ArgumentParser`:: @@ -309,7 +328,8 @@ To change this default behavior, another value can be supplied using the options: -h, --help show this help message and exit -Note that the program name, whether determined from ``sys.argv[0]`` or from the +Note that the program name, whether determined from ``sys.argv[0]``, +from the ``__main__`` module attributes or from the ``prog=`` argument, is available to help messages using the ``%(prog)s`` format specifier. @@ -324,6 +344,8 @@ specifier. -h, --help show this help message and exit --foo FOO foo of the myprogram program +.. versionchanged:: 3.14 + The default value is now not simply ``os.path.basename(sys.argv[0])``. usage ^^^^^ diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index ffc001241ac5ec..4ebf5297d7b4b6 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -202,6 +202,13 @@ New Modules Improved Modules ================ +argparse +-------- + +* The default value of the :ref:`program name ` for + :class:`argparse.ArgumentParser` depends now on the way the Python + interpreter was run. + (Contributed by Serhiy Storchaka and Alyssa Coghlan in :gh:`66436`.) ast --- diff --git a/Lib/argparse.py b/Lib/argparse.py index 874f271959c4fe..24b45d133c3986 100644 --- a/Lib/argparse.py +++ b/Lib/argparse.py @@ -1697,6 +1697,20 @@ def add_mutually_exclusive_group(self, *args, **kwargs): return super().add_mutually_exclusive_group(*args, **kwargs) +def _prog_name(prog=None): + if prog is not None: + return prog + arg0 = _sys.argv[0] + modspec = _sys.modules['__main__'].__spec__ + if modspec is None: + return _os.path.basename(arg0) + py = _os.path.basename(_sys.executable) + if (modspec.name == '__main__' and not modspec.parent and modspec.has_location + and _os.path.dirname(modspec.origin) == _os.path.join(_os.getcwd(), arg0)): + return f'{py} {arg0}' + return f'{py} -m {modspec.name.removesuffix(".__main__")}' + + class ArgumentParser(_AttributeHolder, _ActionsContainer): """Object for parsing command line strings into Python objects. @@ -1740,11 +1754,7 @@ def __init__(self, argument_default=argument_default, conflict_handler=conflict_handler) - # default setting for prog - if prog is None: - prog = _os.path.basename(_sys.argv[0]) - - self.prog = prog + self.prog = _prog_name(prog) self.usage = usage self.epilog = epilog self.formatter_class = formatter_class diff --git a/Lib/test/support/import_helper.py b/Lib/test/support/import_helper.py index edcd2b9a35bbd9..004a680b490b16 100644 --- a/Lib/test/support/import_helper.py +++ b/Lib/test/support/import_helper.py @@ -59,7 +59,7 @@ def make_legacy_pyc(source): """ pyc_file = importlib.util.cache_from_source(source) up_one = os.path.dirname(os.path.abspath(source)) - legacy_pyc = os.path.join(up_one, source + 'c') + legacy_pyc = os.path.join(up_one, os.path.basename(source) + 'c') shutil.move(pyc_file, legacy_pyc) return legacy_pyc diff --git a/Lib/test/test_argparse.py b/Lib/test/test_argparse.py index a972ed0cc9053b..13b30f9e4e40a7 100644 --- a/Lib/test/test_argparse.py +++ b/Lib/test/test_argparse.py @@ -6,6 +6,7 @@ import io import operator import os +import py_compile import shutil import stat import sys @@ -15,10 +16,16 @@ import argparse import warnings -from test.support import os_helper, captured_stderr +from test.support import captured_stderr +from test.support import import_helper +from test.support import os_helper +from test.support import script_helper from unittest import mock +py = os.path.basename(sys.executable) + + class StdIOBuffer(io.TextIOWrapper): '''Replacement for writable io.StringIO that behaves more like real file @@ -2780,7 +2787,7 @@ def setUp(self): group.add_argument('-a', action='store_true') group.add_argument('-b', action='store_true') - self.main_program = os.path.basename(sys.argv[0]) + self.main_program = argparse._prog_name() def test_single_parent(self): parser = ErrorRaisingArgumentParser(parents=[self.wxyz_parent]) @@ -6561,6 +6568,99 @@ def test_os_error(self): self.parser.parse_args, ['@no-such-file']) +class TestProgName(TestCase): + source = textwrap.dedent('''\ + import argparse + parser = argparse.ArgumentParser() + parser.parse_args() + ''') + + def setUp(self): + self.dirname = 'package' + os_helper.FS_NONASCII + self.addCleanup(os_helper.rmtree, self.dirname) + os.mkdir(self.dirname) + + def make_script(self, dirname, basename, *, compiled=False): + script_name = script_helper.make_script(dirname, basename, self.source) + if not compiled: + return script_name + py_compile.compile(script_name, doraise=True) + os.remove(script_name) + pyc_file = import_helper.make_legacy_pyc(script_name) + return pyc_file + + def make_zip_script(self, script_name, name_in_zip=None): + zip_name, _ = script_helper.make_zip_script(self.dirname, 'test_zip', + script_name, name_in_zip) + return zip_name + + def check_usage(self, expected, *args, **kwargs): + res = script_helper.assert_python_ok(*args, '-h', **kwargs) + self.assertEqual(os.fsdecode(res.out.splitlines()[0]), + f'usage: {expected} [-h]') + + def test_script(self, compiled=False): + basename = os_helper.TESTFN + script_name = self.make_script(self.dirname, basename, compiled=compiled) + self.check_usage(os.path.basename(script_name), script_name, '-h') + + def test_script_compiled(self): + self.test_script(compiled=True) + + def test_directory(self, compiled=False): + dirname = os.path.join(self.dirname, os_helper.TESTFN) + os.mkdir(dirname) + self.make_script(dirname, '__main__', compiled=compiled) + self.check_usage(f'{py} {dirname}', dirname) + dirname2 = os.path.join(os.curdir, dirname) + self.check_usage(f'{py} {dirname2}', dirname2) + + def test_directory_compiled(self): + self.test_directory(compiled=True) + + def test_module(self, compiled=False): + basename = 'module' + os_helper.FS_NONASCII + modulename = f'{self.dirname}.{basename}' + self.make_script(self.dirname, basename, compiled=compiled) + self.check_usage(f'{py} -m {modulename}', + '-m', modulename, PYTHONPATH=os.curdir) + + def test_module_compiled(self): + self.test_module(compiled=True) + + def test_package(self, compiled=False): + basename = 'subpackage' + os_helper.FS_NONASCII + packagename = f'{self.dirname}.{basename}' + subdirname = os.path.join(self.dirname, basename) + os.mkdir(subdirname) + self.make_script(subdirname, '__main__', compiled=compiled) + self.check_usage(f'{py} -m {packagename}', + '-m', packagename, PYTHONPATH=os.curdir) + self.check_usage(f'{py} -m {packagename}', + '-m', packagename + '.__main__', PYTHONPATH=os.curdir) + + def test_package_compiled(self): + self.test_package(compiled=True) + + def test_zipfile(self, compiled=False): + script_name = self.make_script(self.dirname, '__main__', compiled=compiled) + zip_name = self.make_zip_script(script_name) + self.check_usage(f'{py} {zip_name}', zip_name) + + def test_zipfile_compiled(self): + self.test_zipfile(compiled=True) + + def test_directory_in_zipfile(self, compiled=False): + script_name = self.make_script(self.dirname, '__main__', compiled=compiled) + name_in_zip = 'package/subpackage/__main__' + ('.py', '.pyc')[compiled] + zip_name = self.make_zip_script(script_name, name_in_zip) + dirname = os.path.join(zip_name, 'package', 'subpackage') + self.check_usage(f'{py} {dirname}', dirname) + + def test_directory_in_zipfile_compiled(self): + self.test_directory_in_zipfile(compiled=True) + + def tearDownModule(): # Remove global references to avoid looking like we have refleaks. RFile.seen = {} diff --git a/Misc/NEWS.d/next/Library/2024-09-30-19-59-28.gh-issue-66436.4gYN_n.rst b/Misc/NEWS.d/next/Library/2024-09-30-19-59-28.gh-issue-66436.4gYN_n.rst new file mode 100644 index 00000000000000..2ea5fb8703c66d --- /dev/null +++ b/Misc/NEWS.d/next/Library/2024-09-30-19-59-28.gh-issue-66436.4gYN_n.rst @@ -0,0 +1,4 @@ +Improved :ref:`prog` default value for :class:`argparse.ArgumentParser`. It +can now include the name of the Python executable along with the module or +package name, or the path to a directory, ZIP file, or directory within a +ZIP file if the code was run that way. From 58cbd648b619ebb2211ab8cd738fa2d2c1d1a98d Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Tue, 1 Oct 2024 09:09:04 +0300 Subject: [PATCH 2/9] Fix encoding/decoding errors on Windows. --- Lib/test/test_argparse.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_argparse.py b/Lib/test/test_argparse.py index 13b30f9e4e40a7..e66c14edd8ed9d 100644 --- a/Lib/test/test_argparse.py +++ b/Lib/test/test_argparse.py @@ -6595,8 +6595,8 @@ def make_zip_script(self, script_name, name_in_zip=None): return zip_name def check_usage(self, expected, *args, **kwargs): - res = script_helper.assert_python_ok(*args, '-h', **kwargs) - self.assertEqual(os.fsdecode(res.out.splitlines()[0]), + res = script_helper.assert_python_ok('-Xutf8', *args, '-h', **kwargs) + self.assertEqual(res.out.splitlines()[0].decode(), f'usage: {expected} [-h]') def test_script(self, compiled=False): From 291b6cfd055c0c64e3e8ed80a14516e76e7041f4 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Tue, 1 Oct 2024 09:19:14 +0300 Subject: [PATCH 3/9] Simplify the the code and fix the directory test on Windows. --- Lib/argparse.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Lib/argparse.py b/Lib/argparse.py index 24b45d133c3986..f0626fbc4024b1 100644 --- a/Lib/argparse.py +++ b/Lib/argparse.py @@ -1705,10 +1705,10 @@ def _prog_name(prog=None): if modspec is None: return _os.path.basename(arg0) py = _os.path.basename(_sys.executable) - if (modspec.name == '__main__' and not modspec.parent and modspec.has_location - and _os.path.dirname(modspec.origin) == _os.path.join(_os.getcwd(), arg0)): - return f'{py} {arg0}' - return f'{py} -m {modspec.name.removesuffix(".__main__")}' + if modspec.name != '__main__': + modname = modspec.name.removesuffix('.__main__') + return f'{py} -m {modname}' + return f'{py} {arg0}' class ArgumentParser(_AttributeHolder, _ActionsContainer): From 93c7a80cc719bbe7235bdac9c212480ca0da4784 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Tue, 1 Oct 2024 09:44:23 +0300 Subject: [PATCH 4/9] Fix test_calendar. --- Lib/test/test_calendar.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_calendar.py b/Lib/test/test_calendar.py index 1f9ffc5e9a5c33..f119d89c0ec39a 100644 --- a/Lib/test/test_calendar.py +++ b/Lib/test/test_calendar.py @@ -985,7 +985,7 @@ def assertFailure(self, *args): def test_help(self): stdout = self.run_cmd_ok('-h') self.assertIn(b'usage:', stdout) - self.assertIn(b'calendar.py', stdout) + self.assertIn(b' -m calendar ', stdout) self.assertIn(b'--help', stdout) # special case: stdout but sys.exit() From 0b72da9dbfdb06f14b0359ca4ae5318cbe1d3802 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Tue, 1 Oct 2024 09:47:34 +0300 Subject: [PATCH 5/9] Try to fix test_parent_help on CI. --- Lib/test/test_argparse.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_argparse.py b/Lib/test/test_argparse.py index e66c14edd8ed9d..acfb7c2863a09a 100644 --- a/Lib/test/test_argparse.py +++ b/Lib/test/test_argparse.py @@ -2878,11 +2878,10 @@ def test_subparser_parents_mutex(self): def test_parent_help(self): parents = [self.abcd_parent, self.wxyz_parent] - parser = ErrorRaisingArgumentParser(parents=parents) + parser = ErrorRaisingArgumentParser(prog='PROG', parents=parents) parser_help = parser.format_help() - progname = self.main_program self.assertEqual(parser_help, textwrap.dedent('''\ - usage: {}{}[-h] [-b B] [--d D] [--w W] [-y Y] a z + usage: PROG [-h] [-b B] [--d D] [--w W] [-y Y] a z positional arguments: a @@ -2898,7 +2897,7 @@ def test_parent_help(self): x: -y Y - '''.format(progname, ' ' if progname else '' ))) + ''')) def test_groups_parents(self): parent = ErrorRaisingArgumentParser(add_help=False) From 5f62bbd2405b3f1f5afa08f2638b601ba278d068 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Tue, 1 Oct 2024 10:09:42 +0300 Subject: [PATCH 6/9] Fix test_embed. --- Lib/argparse.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Lib/argparse.py b/Lib/argparse.py index f0626fbc4024b1..ff779b22e7dac7 100644 --- a/Lib/argparse.py +++ b/Lib/argparse.py @@ -1701,13 +1701,21 @@ def _prog_name(prog=None): if prog is not None: return prog arg0 = _sys.argv[0] - modspec = _sys.modules['__main__'].__spec__ + try: + modspec = _sys.modules['__main__'].__spec__ + except KeyError: + # possibly PYTHONSTARTUP or -X presite + # no good answer here + return _os.path.basename(arg0) if modspec is None: + # simple script return _os.path.basename(arg0) py = _os.path.basename(_sys.executable) if modspec.name != '__main__': + # imported module or package modname = modspec.name.removesuffix('.__main__') return f'{py} -m {modname}' + # directory or ZIP file return f'{py} {arg0}' From 99c7d98cd937b62c0c0680a928ae07205379bc14 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Tue, 1 Oct 2024 13:32:58 +0300 Subject: [PATCH 7/9] Apply suggestions from code review Co-authored-by: Alyssa Coghlan --- Doc/library/argparse.rst | 11 ++++++----- Doc/whatsnew/3.14.rst | 4 ++-- Lib/argparse.py | 6 +++--- .../2024-09-30-19-59-28.gh-issue-66436.4gYN_n.rst | 2 +- 4 files changed, 12 insertions(+), 11 deletions(-) diff --git a/Doc/library/argparse.rst b/Doc/library/argparse.rst index 32cea131415658..83d0a9ed7b1d0a 100644 --- a/Doc/library/argparse.rst +++ b/Doc/library/argparse.rst @@ -273,9 +273,9 @@ to display in help messages depending on the way the Python inerpreter was run: * The :func:`base name ` of ``sys.argv[0]`` if a file was passed as argument. -* The Python inerpreter name followed by ``sys.argv[0]`` if a directory or +* The Python interpreter name followed by ``sys.argv[0]`` if a directory or a zipfile was passed as argument. -* The Python inerpreter name followed by ``-m`` followed by the +* The Python interpreter name followed by ``-m`` followed by the module or package name if the :option:`-m` option was used. This default is almost @@ -289,7 +289,7 @@ named ``myprogram.py`` with the following code:: args = parser.parse_args() The help for this program will display ``myprogram.py`` as the program name -(regardless of where the program was invoked from) if run it as a script: +(regardless of where the program was invoked from) if it is run as a script: .. code-block:: shell-session @@ -307,7 +307,7 @@ The help for this program will display ``myprogram.py`` as the program name -h, --help show this help message and exit --foo FOO foo help -If import it as a module, the help will display a corresponding command line: +If it is executed via the :option:`-m` option, the help will display a corresponding command line: .. code-block:: shell-session @@ -345,7 +345,8 @@ specifier. --foo FOO foo of the myprogram program .. versionchanged:: 3.14 - The default value is now not simply ``os.path.basename(sys.argv[0])``. + The default ``prog`` value now reflects how ``__main__`` was actually executed, + rather than always being ``os.path.basename(sys.argv[0])``. usage ^^^^^ diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index 4ebf5297d7b4b6..67d8d389b58082 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -206,8 +206,8 @@ argparse -------- * The default value of the :ref:`program name ` for - :class:`argparse.ArgumentParser` depends now on the way the Python - interpreter was run. + :class:`argparse.ArgumentParser` now reflects the way the Python + interpreter was instructed to find the ``__main__`` module code. (Contributed by Serhiy Storchaka and Alyssa Coghlan in :gh:`66436`.) ast diff --git a/Lib/argparse.py b/Lib/argparse.py index ff779b22e7dac7..3ab84b922a0be0 100644 --- a/Lib/argparse.py +++ b/Lib/argparse.py @@ -1702,11 +1702,11 @@ def _prog_name(prog=None): return prog arg0 = _sys.argv[0] try: - modspec = _sys.modules['__main__'].__spec__ + modspec = getattr(_sys.modules['__main__'], "__spec__", None) except KeyError: # possibly PYTHONSTARTUP or -X presite - # no good answer here - return _os.path.basename(arg0) + # no good answer here, so fall back to the default + modspec = None if modspec is None: # simple script return _os.path.basename(arg0) diff --git a/Misc/NEWS.d/next/Library/2024-09-30-19-59-28.gh-issue-66436.4gYN_n.rst b/Misc/NEWS.d/next/Library/2024-09-30-19-59-28.gh-issue-66436.4gYN_n.rst index 2ea5fb8703c66d..69a77b01902873 100644 --- a/Misc/NEWS.d/next/Library/2024-09-30-19-59-28.gh-issue-66436.4gYN_n.rst +++ b/Misc/NEWS.d/next/Library/2024-09-30-19-59-28.gh-issue-66436.4gYN_n.rst @@ -1,4 +1,4 @@ Improved :ref:`prog` default value for :class:`argparse.ArgumentParser`. It -can now include the name of the Python executable along with the module or +will now include the name of the Python executable along with the module or package name, or the path to a directory, ZIP file, or directory within a ZIP file if the code was run that way. From 92b9d075b81db98fea637672b4a5e6a21bc807bb Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Tue, 1 Oct 2024 19:06:45 +0300 Subject: [PATCH 8/9] Update Lib/argparse.py --- Lib/argparse.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Lib/argparse.py b/Lib/argparse.py index 3ab84b922a0be0..4b12c2f0c6f857 100644 --- a/Lib/argparse.py +++ b/Lib/argparse.py @@ -1702,9 +1702,9 @@ def _prog_name(prog=None): return prog arg0 = _sys.argv[0] try: - modspec = getattr(_sys.modules['__main__'], "__spec__", None) - except KeyError: - # possibly PYTHONSTARTUP or -X presite + modspec = _sys.modules['__main__'].__spec__ + except (KeyError, AttributeError): + # possibly PYTHONSTARTUP or -X presite or other weird edge case # no good answer here, so fall back to the default modspec = None if modspec is None: From 1f0bfc12b1913d36a1103b30a1667535f379c37a Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Tue, 1 Oct 2024 20:57:26 +0300 Subject: [PATCH 9/9] Try to fix other test on Windows CI. --- Lib/test/test_argparse.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/Lib/test/test_argparse.py b/Lib/test/test_argparse.py index acfb7c2863a09a..057379cec91ba9 100644 --- a/Lib/test/test_argparse.py +++ b/Lib/test/test_argparse.py @@ -2787,8 +2787,6 @@ def setUp(self): group.add_argument('-a', action='store_true') group.add_argument('-b', action='store_true') - self.main_program = argparse._prog_name() - def test_single_parent(self): parser = ErrorRaisingArgumentParser(parents=[self.wxyz_parent]) self.assertEqual(parser.parse_args('-y 1 2 --w 3'.split()), @@ -2907,15 +2905,14 @@ def test_groups_parents(self): m = parent.add_mutually_exclusive_group() m.add_argument('-y') m.add_argument('-z') - parser = ErrorRaisingArgumentParser(parents=[parent]) + parser = ErrorRaisingArgumentParser(prog='PROG', parents=[parent]) self.assertRaises(ArgumentParserError, parser.parse_args, ['-y', 'Y', '-z', 'Z']) parser_help = parser.format_help() - progname = self.main_program self.assertEqual(parser_help, textwrap.dedent('''\ - usage: {}{}[-h] [-w W] [-x X] [-y Y | -z Z] + usage: PROG [-h] [-w W] [-x X] [-y Y | -z Z] options: -h, --help show this help message and exit @@ -2927,7 +2924,7 @@ def test_groups_parents(self): -w W -x X - '''.format(progname, ' ' if progname else '' ))) + ''')) def test_wrong_type_parents(self): self.assertRaises(TypeError, ErrorRaisingArgumentParser, parents=[1])