Skip to content

Commit 9987fa1

Browse files
gh-138697: Fix inferring dest from a single-dash long option in argparse
If a short option and a single-dash long option are passed to add_argument(), dest is now inferred from the single-dash long option.
1 parent 5edfe55 commit 9987fa1

File tree

5 files changed

+48
-21
lines changed

5 files changed

+48
-21
lines changed

Doc/library/argparse.rst

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1313,20 +1313,21 @@ attribute is determined by the ``dest`` keyword argument of
13131313

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

13231323
>>> parser = argparse.ArgumentParser()
13241324
>>> parser.add_argument('-f', '--foo-bar', '--foo')
1325+
>>> parser.add_argument('-q', '-quz')
13251326
>>> parser.add_argument('-x', '-y')
1326-
>>> parser.parse_args('-f 1 -x 2'.split())
1327-
Namespace(foo_bar='1', x='2')
1328-
>>> parser.parse_args('--foo 1 -y 2'.split())
1329-
Namespace(foo_bar='1', x='2')
1327+
>>> parser.parse_args('-f 1 -q 2 -x 3'.split())
1328+
Namespace(foo_bar='1', quz='2', x='3')
1329+
>>> parser.parse_args('--foo 1 -quz 2 -y 3'.split())
1330+
Namespace(foo_bar='1', quz='2', x='2')
13301331

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

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

1339+
.. versionchanged:: next
1340+
Single-dash long option now takes precedence over short options.
1341+
13381342

13391343
.. _deprecated:
13401344

Doc/whatsnew/3.15.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -730,6 +730,13 @@ Porting to Python 3.15
730730
* :meth:`~mmap.mmap.resize` has been removed on platforms that don't support the
731731
underlying syscall, instead of raising a :exc:`SystemError`.
732732

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

734741
Deprecated C APIs
735742
-----------------

Lib/argparse.py

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1668,22 +1668,23 @@ def _get_optional_kwargs(self, *args, **kwargs):
16681668
raise ValueError(
16691669
f'invalid option string {option_string!r}: '
16701670
f'must start with a character {self.prefix_chars!r}')
1671-
1672-
# strings starting with two prefix characters are long options
16731671
option_strings.append(option_string)
1674-
if len(option_string) > 1 and option_string[1] in self.prefix_chars:
1675-
long_option_strings.append(option_string)
16761672

16771673
# infer destination, '--foo-bar' -> 'foo_bar' and '-x' -> 'x'
16781674
dest = kwargs.pop('dest', None)
16791675
if dest is None:
1680-
if long_option_strings:
1681-
dest_option_string = long_option_strings[0]
1682-
else:
1683-
dest_option_string = option_strings[0]
1684-
dest = dest_option_string.lstrip(self.prefix_chars)
1676+
short_option_dest = None
1677+
for option_string in option_strings:
1678+
if len(option_string) > 2:
1679+
# long option: '--foo' or '-foo' -> 'foo'
1680+
dest = option_string.lstrip(self.prefix_chars)
1681+
break
1682+
# short option: '-x' -> 'x'
1683+
if not short_option_dest:
1684+
short_option_dest = option_string.lstrip(self.prefix_chars)
1685+
dest = dest or short_option_dest
16851686
if not dest:
1686-
msg = f'dest= is required for options like {option_string!r}'
1687+
msg = f'dest= is required for options like {repr(option_strings)[1:-1]}'
16871688
raise TypeError(msg)
16881689
dest = dest.replace('-', '_')
16891690

Lib/test/test_argparse.py

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -580,13 +580,22 @@ class TestOptionalsShortLong(ParserTestCase):
580580
class TestOptionalsDest(ParserTestCase):
581581
"""Tests various means of setting destination"""
582582

583-
argument_signatures = [Sig('--foo-bar'), Sig('--baz', dest='zabbaz')]
583+
argument_signatures = [
584+
Sig('-x', '--foo-bar'),
585+
Sig('--baz', dest='zabbaz'),
586+
Sig('-y', '-qux'),
587+
Sig('-z'),
588+
]
584589
failures = ['a']
585590
successes = [
586-
('--foo-bar f', NS(foo_bar='f', zabbaz=None)),
587-
('--baz g', NS(foo_bar=None, zabbaz='g')),
588-
('--foo-bar h --baz i', NS(foo_bar='h', zabbaz='i')),
589-
('--baz j --foo-bar k', NS(foo_bar='k', zabbaz='j')),
591+
('--foo-bar f', NS(foo_bar='f', zabbaz=None, qux=None, z=None)),
592+
('-x f', NS(foo_bar='f', zabbaz=None, qux=None, z=None)),
593+
('--baz g', NS(foo_bar=None, zabbaz='g', qux=None, z=None)),
594+
('--foo-bar h --baz i', NS(foo_bar='h', zabbaz='i', qux=None, z=None)),
595+
('--baz j --foo-bar k', NS(foo_bar='k', zabbaz='j', qux=None, z=None)),
596+
('-qux l', NS(foo_bar=None, zabbaz=None, qux='l', z=None)),
597+
('-y l', NS(foo_bar=None, zabbaz=None, qux='l', z=None)),
598+
('-z m', NS(foo_bar=None, zabbaz=None, qux=None, z='m')),
590599
]
591600

592601

@@ -5608,6 +5617,8 @@ def test_invalid_option_strings(self):
56085617
self.assertTypeError('-', errmsg='dest= is required')
56095618
self.assertTypeError('--', errmsg='dest= is required')
56105619
self.assertTypeError('---', errmsg='dest= is required')
5620+
self.assertTypeError('-', '--', '---',
5621+
errmsg="dest= is required for options like '-', '--', '---'")
56115622

56125623
def test_invalid_prefix(self):
56135624
self.assertValueError('--foo', '+foo',
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Fix inferring *dest* from a single-dash long option in :mod:`argparse`. If a
2+
short option and a single-dash long option are passed to
3+
:meth:`!add_argument()`, *dest* is now inferred from the single-dash long
4+
option.

0 commit comments

Comments
 (0)