Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

move formatting of defaults to FormatterClass#65

Open
MikkelSchubert wants to merge 6 commits into
typed-argparse:mastertyped-argparse/typed-argparse:masterfrom
MikkelSchubert:default_formatter_classMikkelSchubert/typed-argparse:default_formatter_classCopy head branch name to clipboard
Open

move formatting of defaults to FormatterClass#65
MikkelSchubert wants to merge 6 commits into
typed-argparse:mastertyped-argparse/typed-argparse:masterfrom
MikkelSchubert:default_formatter_classMikkelSchubert/typed-argparse:default_formatter_classCopy head branch name to clipboard

Conversation

@MikkelSchubert

Copy link
Copy Markdown
Contributor

This patch moves the formatting of default values into a new FormatterClass that is then set as the default for Parser.

This was originally motivated by the observation that help=SUPPRESS did not work to hide arguments from -h/--help output (due to being mangled by _generate_help_text), and the subsequent realization that you cannot easily override the formatting of default values. Currently you'd have to set auto_default_help for every argument in addition to supplying a FormatterClass.

I tried to keep the auto_default_help functionality intact, but I didn't see obvious way to generalize it. So it'll only affect the formatter class I implemented. I'm not really happy with that part, but personally I'd probably just remove auto_default_help entirely.

- allows SUPPRESS to hide help per argument
- allows overriding the defaults formatting per parser

limit: auto_default_help is only respected by the default FormatterClass
@bluenote10

Copy link
Copy Markdown
Collaborator

This was originally motivated by the observation that help=SUPPRESS did not work to hide arguments from -h/--help output

Do you have an example use case, when suppressing the help text of an argument may be desirable? From a UX perspective this feature looks a bit dubious to me. Why should the help keep information secret from the user / how can the user make use of an argument if it is hidden? Is the main use case when it is so obvious that mentioning it is detrimental?

@MikkelSchubert

Copy link
Copy Markdown
Contributor Author

I've used argparse.SUPPRESS in a few situations:

  1. For options that had been deprecated and (effectively) removed, but where it wasn't desirable to break old scripts still setting those option.

  2. For options that were considered implementation details and were therefore not part of the public (documented) CLI.

  3. And for experimental/unstable features.

@MikkelSchubert

Copy link
Copy Markdown
Contributor Author

The CI is currently failing since the output of examples/high_level_api/basic_usage.py has changed:

 diff --git a/examples/high_level_api/basic_usage.console b/examples/high_level_api/basic_usage.console
 index dda0037..ac5740f 100644
 --- a/examples/high_level_api/basic_usage.console
 +++ b/examples/high_level_api/basic_usage.console
 @@ -8,7 +8,7 @@ optional arguments:
    --my-arg MY_ARG       some help
    --number-a NUMBER_A   some help [default: 42]
    --number-b NUMBER_B   some help
 -  --verbose             some help
 +  --verbose             some help [default: False]
    --names [NAMES [NAMES ...]]
                          some help

The reason is that help text is now generated later, from an Action object passed to HelpFormatter._get_help_string. Here the default value will have been set to either True or False depending on the action (_StoreTrue or _StoreFalse). Previously the help text was generated directly from the Arg object, where we could observe the user-supplied default value (if any).

It doesn't seem possible to recreate the old behavior exactly, but I am also not sure it is desirable to recreate as since it is somewhat inconsistent: The args x: bool = arg(help="foo") and x: bool = arg(help="foo", default=True) work the same, but only the latter would have a [Default: True] text.

The two options I can see are a) accept the change and always print a default for boolean options (unless disabled via auto_default_help), or b) don't print a default for boolean options, similar to how None defaults are treated.

Personally I'm more in favor of not showing the default for booleans. The effect of setting a boolean option will normally be explained by the help text and adding a [default: True] or [default: False] doesn't add any useful information. This would be a implemented as a additional check in DefaultHelpFormatter._get_help_string and could therefore easily be overridden.

@bluenote10

Copy link
Copy Markdown
Collaborator

First of all, sorry for the delay on this one! I was offline for a few days, and didn't had time to look into it in detail yet.

Personally I'm more in favor of not showing the default for booleans.

Yes I'd also say that omitting default values in general for switches is more sensible. Especially with negated flags, like say --no-install it would be confusing if the help text says [default: False], because the default actually refers to the non-negated internal variable, and not the already negated name -- no need to confuse the user with potential double negation interpretation... It is a more of an accident currently that they only show up in the explicit case.

not isinstance(action.help, DefaultHelpFormatter.Unformatted)
and action.help is not None
and action.default is not None
and action.default is not argparse.SUPPRESS

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are a few things that I don't understand here yet:

  1. It looks like we are going into the else branch if help is DefaultHelpFormatter.Unformatted or if it is None, right? Doesn't that mean that DefaultHelpFormatter.Unformatted is unnecessary? I.e., could the user achieve the same by setting help to None anyway?

  2. What is the purpose of comparing action.default to argparse.SUPPRESS? In general it seems that argparse.SUPPRESS has multiple roles in argparse:

  • It can be used as a value for the help argument to suppress the help text.
  • It also can be used for the argument_default and default arguments (for the parser or add_argument). This usage has a special meaning that the field will be omitted from the argparse.Namespace instance.

The latter usage is problematic from the typed_argparse perspective, because we rely on arguments always being generated in the Namespace instance. Using argparse.SUPPRESS in the default position actually breaks argument parsing currently:

import argparse
from typing import Optional

import typed_argparse as tap


class Args(tap.TypedArgs):
    foo: Optional[str] = tap.arg(default=argparse.SUPPRESS)


def run(args: Args):
    print(args)


if __name__ == "__main__":
    tap.Parser(Args).bind(run).run()

Fortunately it doesn't type check for other types than str, because its a string based sentinel value. I'll probably have to catch that, i.e., the default value '==SUPPRESS==' for strings is kinda forbidden 😕

This makes we wonder why to compare with action.default here though. I would have understood if there is a comparison like action.help == argparse.SUPPRESS but surprisingly that doesn't seem necessary - so the filtering actually happens internally in argparse? If so, shouldn't we remove action.default is not argparse.SUPPRESS from the condition, if we cannot support it in the default position anyway?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't worry finding the time for this. There's no hurry.

  1. DefaultHelpFormatter.Unformatted is a bit of a hack to support the auto_default_help option and is only used if help is not None (see https://github.com/MikkelSchubert/typed-argparse/blob/8a76abd4166a1bcbdd16554ce56851516bcb42be/typed_argparse/parser.py#L480). The problem is that auto_default_help isn't available to the HelpFormatter instance, so instead I convert the (not None) help string to an instance of a string subclass. This allows me to check whether or not the user set auto_default_help=True without breaking stuff in argparse, where help is expected to be a string. There may be a better way of doing this, but I couldn't figure one out.

  2. I agree with all your points regarding argparse.SUPPRESS, but it needs to be handled here simply because argparse makes use of it even if we don't. I should probably add a comment about that. If you don't check for it in the HelpFormatter then you get output like this from (from an unmodified examples/high_level_api/basic_usage.py):

       -h, --help           show this help message and exit [default: ==SUPPRESS==]
    

@codecov-commenter

codecov-commenter commented Sep 4, 2023

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 89.47368% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 93.00%. Comparing base (cef3707) to head (46a585a).
⚠️ Report is 12 commits behind head on master.

Files with missing lines Patch % Lines
typed_argparse/formatter.py 84.61% 2 Missing ⚠️
❗ Your organization needs to install the Codecov GitHub app to enable full functionality.
Additional details and impacted files
@@            Coverage Diff             @@
##           master      #65      +/-   ##
==========================================
+ Coverage   92.89%   93.00%   +0.10%     
==========================================
  Files           9       10       +1     
  Lines         704      715      +11     
==========================================
+ Hits          654      665      +11     
  Misses         50       50              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

Morty Proxy This is a proxified and sanitized view of the page, visit original site.