Skip to content

Argparse: certain action types dont accept metavar, but should? #109792

Description

@DanCardin

Feature or enhancement

Proposal:

Given some arbitrary argument with a dest value of foo.bar.baz (justification below), you will end up with helptext like FOO.BAR.BAZ. This is the explicit usecase for the use of metavar. You set metavar to the value you want it to show up as, and you're good.

Unfortunately, you aren't allowed to supply metavar in some scenarios. I haven't enumerated all of them yet, but, for example, if you set action='help', action='store_true', or action='store_false', you'll be routed through _HelpAction, _StoreTrueAction, or _StoreFalseAction, none of which accept a metavar argument. There may be more.

The superclasses of all three do accept metavar, but in usercode i can just subclass them like so:

class _HelpAction(argparse._HelpAction):
    def __init__(self, metavar=None, **kwargs):
        self.metavar = metavar
        super().__init__(**kwargs)


class _StoreTrueAction(argparse._StoreTrueAction):
    def __init__(self, metavar=None, **kwargs):
        self.metavar = metavar
        super().__init__(**kwargs)


class _StoreFalseAction(argparse._StoreFalseAction):
    def __init__(self, metavar=None, **kwargs):
        self.metavar = metavar
        super().__init__(**kwargs)

class ArgumentParser(argparse.ArgumentParser):
    def __init__(self, *args, **kwargs):
        self.register("action", "store_true", _StoreTrueAction)
        self.register("action", "store_false", _StoreFalseAction)

With no other changes, the rendered helptext names of the options properly accept and render the metavar value instead of the dest.


Some context/justification for why this is meaningful. I am currently using this snippet from the internet to, with relatively little effort, "properly" parse multiply nested subparsers in a way that maintains the nested relationship of the input args when they're written:

class Nestedspace(argparse.Namespace):
    def __setattr__(self, name, value):
        if "." in name:
            group, name = name.split(".", 1)
            ns = getattr(self, group, Nestedspace())
            setattr(ns, name, value)
            self.__dict__[group] = ns
        else:
            self.__dict__[name] = value

Which produces {'foo': {'bar': {'baz': 4}}} for some doubly nested subparser with a baz argument. Whereas it'd have just produced {'baz': 4} by "default".

This strategy requires setting the dest value to foo.bar.baz for the arg in question.

Having done that, you end up with [-k FOO.BAR.BAZ] in the help text, with no way to customize it.


Has this already been discussed elsewhere?

This is a minor feature, which does not need previous discussion elsewhere

Links to previous discussion of this feature:

I dont know whether this is relevant to issues like #103678, where they're removing metavar from not-these-classes-but-boolean-action-option, which naively seems related. perhaps it's unrelated though.


if it were just a matter of making the in-repo equivalents to these changes (plus tests), i'd be willing to contribue them, but i figured it might be this was for a reason, and wanted to make sure it'd be accepted before submitting a PR

Activity

  1. added
    stdlibStandard Library Python modules in the Lib/ directory
    on Sep 24, 2023
  2. serhiy-storchaka commented on Sep 28, 2024

    @serhiy-storchaka
    Member

    metavar is used to format the help output and some error messages. Neither of the actions mention above has argument, they do not need metavar. How did you get [-k FOO.BAR.BAZ] in the help text for any of these actions?

  3. DanCardin commented on Sep 30, 2024

    @DanCardin
    Author

    I'm...actually not sure how, given my example i would have gotten -k <something>.

    Fwiw, this is/was in the context of writing a CLI library wrapping argparse. My original reason for wanting this is probably less relevant, because I now replace the argparse --help text also. With that said, i do see some places where metavar is used in error messages.

    Perhaps this isn't actually a relevant problem to real, working CLIs in practice (I know I reported this in response to a real-world case, but I can't point to what that case was now, unfortunately.

    It is possible to craft CLIs (if not useful ones) that expose the argument name (which, given my motivating example in the OP, is problematic and would ideally be the metavar):

    import argparse
    
    parser = argparse.ArgumentParser()
    parser.add_argument('foo', action='store_true')
    print(parser.parse_args())

    It certainly is simpler as a lower level programmatic user of the library for all action types to accept the same set of base-level (those which directly inform the parser of how to act, like option_strings, dest, required, etc) arguments for all action types, regardless of whether or not they're "practical".


    With that said, i also have my workaround at this point so it's not particularly burdensome to me, and it is admittedly a niche problem

  4. serhiy-storchaka commented on Oct 9, 2024

    @serhiy-storchaka
    Member

    Actions like 'store_true' do not consume arguments. Using them with positional arguments is meaningless and will fail when try to parse arguments. See also #85935.

    Closing this issue due to a lack of useful cases.

  5. DanCardin commented on Oct 9, 2024

    @DanCardin
    Author

    Mostly, I was just pointing out a place where there seems to be extra effort taken to omit parameters that dont have an obvious use (but also there's no negative to specifying them).

    Where _StoreTrueAction could (modulo "breaking" changes to a private type) literally just be _StoreTrueAction = functools.partial(_StoreConstAction, const=True) behavior-wise. Because _StoreConstAction which applies to all the same of your arguments, does accept metavar.

    And I know that cappa (a cli library with optional argparse backend), at least was using them to internally track something, although it doesn't seem to now... And it does make it easier to programmatically interact with the argparse api, without special casing each action type. With that said, the usecase is relatively niche, and it's easy enough for me to subclass and ignore the intended argparse interface.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    pendingThe issue will be closed if no feedback is providedstdlibStandard Library Python modules in the Lib/ directorytype-featureA feature request or enhancement

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions