mirror of https://github.com/python/cpython
gh-66436: Improved prog default value for argparse.ArgumentParser (GH-124799)
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 Co-authored-by: Alyssa Coghlan <ncoghlan@gmail.com>
This commit is contained in:
parent
d150e4abcf
commit
04bfea2d26
|
@ -30,7 +30,7 @@ Quick Links for ArgumentParser
|
||||||
========================= =========================================================================================================== ==================================================================================
|
========================= =========================================================================================================== ==================================================================================
|
||||||
Name Description Values
|
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
|
usage_ The string describing the program usage
|
||||||
description_ A brief description of what the program does
|
description_ A brief description of what the program does
|
||||||
epilog_ Additional description of the program after the argument help
|
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
|
as keyword arguments. Each parameter has its own more detailed description
|
||||||
below, but in short they are:
|
below, but in short they are:
|
||||||
|
|
||||||
* prog_ - The name of the program (default:
|
* prog_ - The name of the program (default: generated from the ``__main__``
|
||||||
``os.path.basename(sys.argv[0])``)
|
module attributes and ``sys.argv[0]``)
|
||||||
|
|
||||||
* usage_ - The string describing the program usage (default: generated from
|
* usage_ - The string describing the program usage (default: generated from
|
||||||
arguments added to parser)
|
arguments added to parser)
|
||||||
|
@ -268,10 +268,18 @@ The following sections describe how each of these are used.
|
||||||
prog
|
prog
|
||||||
^^^^
|
^^^^
|
||||||
|
|
||||||
By default, :class:`ArgumentParser` objects use the base name
|
By default, :class:`ArgumentParser` calculates the name of the program
|
||||||
(see :func:`os.path.basename`) of ``sys.argv[0]`` to determine
|
to display in help messages depending on the way the Python inerpreter was run:
|
||||||
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
|
* The :func:`base name <os.path.basename>` of ``sys.argv[0]`` if a file was
|
||||||
|
passed as argument.
|
||||||
|
* The Python interpreter name followed by ``sys.argv[0]`` if a directory or
|
||||||
|
a zipfile was passed as argument.
|
||||||
|
* 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
|
||||||
|
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
|
used to invoke the program on the command line. For example, consider a file
|
||||||
named ``myprogram.py`` with the following code::
|
named ``myprogram.py`` with the following code::
|
||||||
|
|
||||||
|
@ -281,7 +289,7 @@ named ``myprogram.py`` with the following code::
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
The help for this program will display ``myprogram.py`` as the program name
|
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 it is run as a script:
|
||||||
|
|
||||||
.. code-block:: shell-session
|
.. 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
|
-h, --help show this help message and exit
|
||||||
--foo FOO foo help
|
--foo FOO foo help
|
||||||
|
|
||||||
|
If it is executed via the :option:`-m` option, 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
|
To change this default behavior, another value can be supplied using the
|
||||||
``prog=`` argument to :class:`ArgumentParser`::
|
``prog=`` argument to :class:`ArgumentParser`::
|
||||||
|
|
||||||
|
@ -309,7 +328,8 @@ To change this default behavior, another value can be supplied using the
|
||||||
options:
|
options:
|
||||||
-h, --help show this help message and exit
|
-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
|
``prog=`` argument, is available to help messages using the ``%(prog)s`` format
|
||||||
specifier.
|
specifier.
|
||||||
|
|
||||||
|
@ -324,6 +344,9 @@ specifier.
|
||||||
-h, --help show this help message and exit
|
-h, --help show this help message and exit
|
||||||
--foo FOO foo of the myprogram program
|
--foo FOO foo of the myprogram program
|
||||||
|
|
||||||
|
.. versionchanged:: 3.14
|
||||||
|
The default ``prog`` value now reflects how ``__main__`` was actually executed,
|
||||||
|
rather than always being ``os.path.basename(sys.argv[0])``.
|
||||||
|
|
||||||
usage
|
usage
|
||||||
^^^^^
|
^^^^^
|
||||||
|
|
|
@ -202,6 +202,13 @@ New Modules
|
||||||
Improved Modules
|
Improved Modules
|
||||||
================
|
================
|
||||||
|
|
||||||
|
argparse
|
||||||
|
--------
|
||||||
|
|
||||||
|
* The default value of the :ref:`program name <prog>` for
|
||||||
|
: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
|
ast
|
||||||
---
|
---
|
||||||
|
|
|
@ -1697,6 +1697,28 @@ class _MutuallyExclusiveGroup(_ArgumentGroup):
|
||||||
return super().add_mutually_exclusive_group(*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]
|
||||||
|
try:
|
||||||
|
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:
|
||||||
|
# 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}'
|
||||||
|
|
||||||
|
|
||||||
class ArgumentParser(_AttributeHolder, _ActionsContainer):
|
class ArgumentParser(_AttributeHolder, _ActionsContainer):
|
||||||
"""Object for parsing command line strings into Python objects.
|
"""Object for parsing command line strings into Python objects.
|
||||||
|
|
||||||
|
@ -1740,11 +1762,7 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer):
|
||||||
argument_default=argument_default,
|
argument_default=argument_default,
|
||||||
conflict_handler=conflict_handler)
|
conflict_handler=conflict_handler)
|
||||||
|
|
||||||
# default setting for prog
|
self.prog = _prog_name(prog)
|
||||||
if prog is None:
|
|
||||||
prog = _os.path.basename(_sys.argv[0])
|
|
||||||
|
|
||||||
self.prog = prog
|
|
||||||
self.usage = usage
|
self.usage = usage
|
||||||
self.epilog = epilog
|
self.epilog = epilog
|
||||||
self.formatter_class = formatter_class
|
self.formatter_class = formatter_class
|
||||||
|
|
|
@ -6,6 +6,7 @@ import inspect
|
||||||
import io
|
import io
|
||||||
import operator
|
import operator
|
||||||
import os
|
import os
|
||||||
|
import py_compile
|
||||||
import shutil
|
import shutil
|
||||||
import stat
|
import stat
|
||||||
import sys
|
import sys
|
||||||
|
@ -15,10 +16,16 @@ import unittest
|
||||||
import argparse
|
import argparse
|
||||||
import warnings
|
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
|
from unittest import mock
|
||||||
|
|
||||||
|
|
||||||
|
py = os.path.basename(sys.executable)
|
||||||
|
|
||||||
|
|
||||||
class StdIOBuffer(io.TextIOWrapper):
|
class StdIOBuffer(io.TextIOWrapper):
|
||||||
'''Replacement for writable io.StringIO that behaves more like real file
|
'''Replacement for writable io.StringIO that behaves more like real file
|
||||||
|
|
||||||
|
@ -2780,8 +2787,6 @@ class TestParentParsers(TestCase):
|
||||||
group.add_argument('-a', action='store_true')
|
group.add_argument('-a', action='store_true')
|
||||||
group.add_argument('-b', action='store_true')
|
group.add_argument('-b', action='store_true')
|
||||||
|
|
||||||
self.main_program = os.path.basename(sys.argv[0])
|
|
||||||
|
|
||||||
def test_single_parent(self):
|
def test_single_parent(self):
|
||||||
parser = ErrorRaisingArgumentParser(parents=[self.wxyz_parent])
|
parser = ErrorRaisingArgumentParser(parents=[self.wxyz_parent])
|
||||||
self.assertEqual(parser.parse_args('-y 1 2 --w 3'.split()),
|
self.assertEqual(parser.parse_args('-y 1 2 --w 3'.split()),
|
||||||
|
@ -2871,11 +2876,10 @@ class TestParentParsers(TestCase):
|
||||||
|
|
||||||
def test_parent_help(self):
|
def test_parent_help(self):
|
||||||
parents = [self.abcd_parent, self.wxyz_parent]
|
parents = [self.abcd_parent, self.wxyz_parent]
|
||||||
parser = ErrorRaisingArgumentParser(parents=parents)
|
parser = ErrorRaisingArgumentParser(prog='PROG', parents=parents)
|
||||||
parser_help = parser.format_help()
|
parser_help = parser.format_help()
|
||||||
progname = self.main_program
|
|
||||||
self.assertEqual(parser_help, textwrap.dedent('''\
|
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:
|
positional arguments:
|
||||||
a
|
a
|
||||||
|
@ -2891,7 +2895,7 @@ class TestParentParsers(TestCase):
|
||||||
|
|
||||||
x:
|
x:
|
||||||
-y Y
|
-y Y
|
||||||
'''.format(progname, ' ' if progname else '' )))
|
'''))
|
||||||
|
|
||||||
def test_groups_parents(self):
|
def test_groups_parents(self):
|
||||||
parent = ErrorRaisingArgumentParser(add_help=False)
|
parent = ErrorRaisingArgumentParser(add_help=False)
|
||||||
|
@ -2901,15 +2905,14 @@ class TestParentParsers(TestCase):
|
||||||
m = parent.add_mutually_exclusive_group()
|
m = parent.add_mutually_exclusive_group()
|
||||||
m.add_argument('-y')
|
m.add_argument('-y')
|
||||||
m.add_argument('-z')
|
m.add_argument('-z')
|
||||||
parser = ErrorRaisingArgumentParser(parents=[parent])
|
parser = ErrorRaisingArgumentParser(prog='PROG', parents=[parent])
|
||||||
|
|
||||||
self.assertRaises(ArgumentParserError, parser.parse_args,
|
self.assertRaises(ArgumentParserError, parser.parse_args,
|
||||||
['-y', 'Y', '-z', 'Z'])
|
['-y', 'Y', '-z', 'Z'])
|
||||||
|
|
||||||
parser_help = parser.format_help()
|
parser_help = parser.format_help()
|
||||||
progname = self.main_program
|
|
||||||
self.assertEqual(parser_help, textwrap.dedent('''\
|
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:
|
options:
|
||||||
-h, --help show this help message and exit
|
-h, --help show this help message and exit
|
||||||
|
@ -2921,7 +2924,7 @@ class TestParentParsers(TestCase):
|
||||||
|
|
||||||
-w W
|
-w W
|
||||||
-x X
|
-x X
|
||||||
'''.format(progname, ' ' if progname else '' )))
|
'''))
|
||||||
|
|
||||||
def test_wrong_type_parents(self):
|
def test_wrong_type_parents(self):
|
||||||
self.assertRaises(TypeError, ErrorRaisingArgumentParser, parents=[1])
|
self.assertRaises(TypeError, ErrorRaisingArgumentParser, parents=[1])
|
||||||
|
@ -6561,6 +6564,99 @@ class TestExitOnError(TestCase):
|
||||||
self.parser.parse_args, ['@no-such-file'])
|
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('-Xutf8', *args, '-h', **kwargs)
|
||||||
|
self.assertEqual(res.out.splitlines()[0].decode(),
|
||||||
|
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():
|
def tearDownModule():
|
||||||
# Remove global references to avoid looking like we have refleaks.
|
# Remove global references to avoid looking like we have refleaks.
|
||||||
RFile.seen = {}
|
RFile.seen = {}
|
||||||
|
|
|
@ -985,7 +985,7 @@ class CommandLineTestCase(unittest.TestCase):
|
||||||
def test_help(self):
|
def test_help(self):
|
||||||
stdout = self.run_cmd_ok('-h')
|
stdout = self.run_cmd_ok('-h')
|
||||||
self.assertIn(b'usage:', stdout)
|
self.assertIn(b'usage:', stdout)
|
||||||
self.assertIn(b'calendar.py', stdout)
|
self.assertIn(b' -m calendar ', stdout)
|
||||||
self.assertIn(b'--help', stdout)
|
self.assertIn(b'--help', stdout)
|
||||||
|
|
||||||
# special case: stdout but sys.exit()
|
# special case: stdout but sys.exit()
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
Improved :ref:`prog` default value for :class:`argparse.ArgumentParser`. It
|
||||||
|
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.
|
Loading…
Reference in New Issue