Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions Doc/library/argparse.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1313,20 +1313,21 @@ attribute is determined by the ``dest`` keyword argument of

For optional argument actions, the value of ``dest`` is normally inferred from
the option strings. :class:`ArgumentParser` generates the value of ``dest`` by
taking the first long option string and stripping away the initial ``--``
string. If no long option strings were supplied, ``dest`` will be derived from
taking the first long option string and stripping away the initial ``-``
characters. If no long option strings were supplied, ``dest`` will be derived from
the first short option string by stripping the initial ``-`` character. Any
internal ``-`` characters will be converted to ``_`` characters to make sure
the string is a valid attribute name. The examples below illustrate this
behavior::

>>> parser = argparse.ArgumentParser()
>>> parser.add_argument('-f', '--foo-bar', '--foo')
>>> parser.add_argument('-q', '-quz')
>>> parser.add_argument('-x', '-y')
>>> parser.parse_args('-f 1 -x 2'.split())
Namespace(foo_bar='1', x='2')
>>> parser.parse_args('--foo 1 -y 2'.split())
Namespace(foo_bar='1', x='2')
>>> parser.parse_args('-f 1 -q 2 -x 3'.split())
Namespace(foo_bar='1', quz='2', x='3')
>>> parser.parse_args('--foo 1 -quz 2 -y 3'.split())
Namespace(foo_bar='1', quz='2', x='2')

``dest`` allows a custom attribute name to be provided::

Expand All @@ -1335,6 +1336,9 @@ behavior::
>>> parser.parse_args('--foo XXX'.split())
Namespace(bar='XXX')

.. versionchanged:: next
Single-dash long option now takes precedence over short options.


.. _deprecated:

Expand Down
7 changes: 7 additions & 0 deletions Doc/whatsnew/3.15.rst
Original file line number Diff line number Diff line change
Expand Up @@ -730,6 +730,13 @@ Porting to Python 3.15
* :meth:`~mmap.mmap.resize` has been removed on platforms that don't support the
underlying syscall, instead of raising a :exc:`SystemError`.

* If a short option and a single-dash long option are passed to
:meth:`argparse.ArgumentParser.add_argument`, *dest* is inferred from
the single-dash long option. For example, in ``add_argument('-f', '-foo')``,
*dest* is now ``'foo'`` instead of ``'f'``.
Pass an explicit *dest* argument to preserve the old behavior.
(Contributed by Serhiy Storchaka in :gh:`138697`.)


Deprecated C APIs
-----------------
Expand Down
21 changes: 11 additions & 10 deletions Lib/argparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -1668,22 +1668,23 @@ def _get_optional_kwargs(self, *args, **kwargs):
raise ValueError(
f'invalid option string {option_string!r}: '
f'must start with a character {self.prefix_chars!r}')

# strings starting with two prefix characters are long options
option_strings.append(option_string)
if len(option_string) > 1 and option_string[1] in self.prefix_chars:
long_option_strings.append(option_string)

# infer destination, '--foo-bar' -> 'foo_bar' and '-x' -> 'x'
dest = kwargs.pop('dest', None)
if dest is None:
if long_option_strings:
dest_option_string = long_option_strings[0]
else:
dest_option_string = option_strings[0]
dest = dest_option_string.lstrip(self.prefix_chars)
short_option_dest = None
for option_string in option_strings:
if len(option_string) > 2:
# long option: '--foo' or '-foo' -> 'foo'
dest = option_string.lstrip(self.prefix_chars)
break
# short option: '-x' -> 'x'
if not short_option_dest:
short_option_dest = option_string.lstrip(self.prefix_chars)
dest = dest or short_option_dest
if not dest:
msg = f'dest= is required for options like {option_string!r}'
msg = f'dest= is required for options like {repr(option_strings)[1:-1]}'
raise TypeError(msg)
dest = dest.replace('-', '_')

Expand Down
21 changes: 16 additions & 5 deletions Lib/test/test_argparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -580,13 +580,22 @@ class TestOptionalsShortLong(ParserTestCase):
class TestOptionalsDest(ParserTestCase):
"""Tests various means of setting destination"""

argument_signatures = [Sig('--foo-bar'), Sig('--baz', dest='zabbaz')]
argument_signatures = [
Sig('-x', '--foo-bar'),
Sig('--baz', dest='zabbaz'),
Sig('-y', '-qux'),
Sig('-z'),
]
failures = ['a']
successes = [
('--foo-bar f', NS(foo_bar='f', zabbaz=None)),
('--baz g', NS(foo_bar=None, zabbaz='g')),
('--foo-bar h --baz i', NS(foo_bar='h', zabbaz='i')),
('--baz j --foo-bar k', NS(foo_bar='k', zabbaz='j')),
('--foo-bar f', NS(foo_bar='f', zabbaz=None, qux=None, z=None)),
('-x f', NS(foo_bar='f', zabbaz=None, qux=None, z=None)),
('--baz g', NS(foo_bar=None, zabbaz='g', qux=None, z=None)),
('--foo-bar h --baz i', NS(foo_bar='h', zabbaz='i', qux=None, z=None)),
('--baz j --foo-bar k', NS(foo_bar='k', zabbaz='j', qux=None, z=None)),
('-qux l', NS(foo_bar=None, zabbaz=None, qux='l', z=None)),
('-y l', NS(foo_bar=None, zabbaz=None, qux='l', z=None)),
('-z m', NS(foo_bar=None, zabbaz=None, qux=None, z='m')),
]


Expand Down Expand Up @@ -5608,6 +5617,8 @@ def test_invalid_option_strings(self):
self.assertTypeError('-', errmsg='dest= is required')
self.assertTypeError('--', errmsg='dest= is required')
self.assertTypeError('---', errmsg='dest= is required')
self.assertTypeError('-', '--', '---',
errmsg="dest= is required for options like '-', '--', '---'")

def test_invalid_prefix(self):
self.assertValueError('--foo', '+foo',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Fix inferring *dest* from a single-dash long option in :mod:`argparse`. If a
short option and a single-dash long option are passed to
:meth:`!add_argument`, *dest* is now inferred from the single-dash long
option.
Loading