sphinx-doc / sphinx-argparse

A Sphinx extension to automatically document argparse commands and options
https://sphinx-argparse.readthedocs.org/
MIT License
32 stars 25 forks source link

Add support for `type=<class Enum>` to list the possible choices. #47

Closed raven42 closed 6 months ago

raven42 commented 8 months ago

The python add_argument() method supports a type field which can be set to an Enum class object. Then using the metavar field it can be defined to mimic the choices field. Then the prog --help shows the correct values, and this includes the basic usage in the sphinx-argparse output as well.

However the individual parameter listing for sphinx-argparse does not list the possible choices in the same way as it does for the choices field.

This will enable that functionality where if the type is set to an EnumType instance, then it will append the Possible Choices list if there is no choices defined already.


"""
test.py

.. argparse::
    :ref: test.argParser
    :prog: test.py
"""

import argparse
from enum import Enum, auto

class someStringEnum(str, Enum):
    value1 = 'value1'
    value2 = 'value2'
    value3 = 'value3'

class someIntEnum(str, Enum):
    int1 = auto()
    int2 = auto()
    int3 = auto()

def argParser():
    parser = argparse.ArgumentParser('description')
    parser.add_argument('--string', type=someStringEnum, help='String based enum', metavar='{' + ','.join(someStringEnum) + '}')
    parser.add_argument('--int', type=someIntEnum, help='Int based enum', metavar='{' + ','.join(someIntEnum) + '}')
    return parser

def main():
    parser = argParser()
    args = parser.parse_args()
    print(args)

if __name__ == '__main__':
    main()
$ test.py --help
usage: description [-h] [--string {value1,value2,value3}] [--int {1,2,3}]

options:
    -h, --help            show this help message and exit
    --string {value1,value2,value3}
                          String based enum
    --int {1,2,3}         Int based enum
$ test.py --string unknown
usage: description [-h] [--string {value1,value2,value3}] [--int {1,2,3}]
description: error: argument --string: invalid someStringEnum value: 'unknown'
$ test.py --string value1 --int 2
Namespace(string=<someStringEnum.value1: 'value1'>, int=<someIntEnum.int2: '2'>)
$

Output Example: image

raven42 commented 8 months ago

workflow appears to be failing due to an unknown package 'commonmark'. Does not appear related to my PR

Running Sphinx v7.2.6
making output directory... done
building [mo]: targets for 0 po files that are out of date
writing output... 
building [html]: targets for 9 source files that are out of date
updating environment: [new config] 9 added, 0 changed, 0 removed
reading sources... [ 11%] changelog
reading sources... [ 22%] contrib
reading sources... [ 33%] extend
reading sources... [ 44%] index
reading sources... [ 56%] install
reading sources... [ 67%] markdown
Traceback (most recent call last):
  File "/home/docs/checkouts/readthedocs.org/user_builds/sphinx-argparse/envs/47/lib/python3.12/site-packages/sphinxarg/markdown.py", line 2, in <module>
    from commonmark import Parser
ModuleNotFoundError: No module named 'commonmark'
ashb commented 6 months ago

I don't think I've ever seen anyone use the metavar like that.

Is that something that the Argparse docs recommend?

raven42 commented 6 months ago

I don't think I've ever seen anyone use the metavar like that.

Is that something that the Argparse docs recommend?

I use it somewhat frequently in my scripts. I've found it is only needed with the type=<enum> syntax as by default argparse doesn't treat it quite like the choices=[<list of values>].

Originally I had used the choices but the help output isn't very clear. It only shows the field name in all caps when using type=<Enum>, and I want it to look like the same output for choices=[<list>]. For example:

#!/usr/bin/env python3
"""
test.py

.. argparse::
    :ref: test.argParser
    :prog: test.py
"""

import argparse
from enum import Enum, auto

class someStringEnum(str, Enum):
    value1 = 'value1'
    value2 = 'value2'
    value3 = 'value3'

class someIntEnum(str, Enum):
    int1 = auto()
    int2 = auto()
    int3 = auto()

def argParser():
    parser = argparse.ArgumentParser('description')
    parser.add_argument('--string', type=someStringEnum, help='String based enum')
    parser.add_argument('--int', type=someIntEnum, help='Int based enum')
    parser.add_argument('--other-choice', choices=[1, 2, 3], help='Normal list of choices')
    parser.add_argument('--string-choice', choices=[v for v in someStringEnum], help='String based choice')
    parser.add_argument('--int-choice', choices=[v for v in someIntEnum], metavar='{' + ','.join(someIntEnum) + '}',
                        help='String based choice')
    return parser

def main():
    parser = argParser()
    args = parser.parse_args()
    print(args)

if __name__ == '__main__':
    main()

This will yield the following:

$ test.py --help
usage: description [-h] [--string STRING] [--int INT] [--other-choice {1,2,3}]
                   [--string-choice {someStringEnum.value1,someStringEnum.value2,someStringEnum.value3}]
                   [--int-choice {1,2,3}]

options:
  -h, --help            show this help message and exit
  --string STRING       String based enum
  --int INT             Int based enum
  --other-choice {1,2,3}
                        Normal list of choices
  --string-choice {someStringEnum.value1,someStringEnum.value2,someStringEnum.value3}
                        String based choice
  --int-choice {1,2,3}  String based choice
$ test.py --int-choice 1
Namespace(string=None, int=None, string_choice=None, int_choice='1')
$ test.py --int-choice 10
usage: description [-h] [--string STRING] [--int INT] [--other-choice {1,2,3}]
                   [--string-choice {someStringEnum.value1,someStringEnum.value2,someStringEnum.value3}]
                   [--int-choice {someIntEnum.int1,someIntEnum.int2,someIntEnum.int3}]
description: error: argument --int-choice: invalid choice: '10' (choose from <someIntEnum.int1: '1'>, <someIntEnum.int2: '2'>, <someIntEnum.int3: '3'>)
$

So when just using choices=[v for v in <Enum>] the enforcement works fine, but the look from the usage statement is not meaningful as a user that doesn't know the actual values of those Enum instances won't be able to really use it.

The metavar=<string> is a easy way to get the usage statement to show what you want it to show to help the user. This is fully documented in the standard python library documentation here: https://docs.python.org/3/library/argparse.html#metavar

In any of these cases though, the sphinx-argparse will format it just like the normal usage statement, except the metavar doesn't get used by sphinx-argparse. So even if I were to use the metavar along with the choices like above for the --int-choice parameter, sphinx-argparse still doesn't display the metavar value and so shows the possible choices as someIntEnum.int1, someIntEnum.int2, ... in the Named Arguments section. Though note, the usage DOES display correctly.

image

So this PR will allow the usage of type=<Enum> and will format both correctly for sphinx-argparse, but then also using the metavar=... allows the usage statement to be rendered correctly from the script command line args as well.

ashb commented 6 months ago

The docs on choices don't show anything about using metavar like that

https://docs.python.org/3/library/argparse.html#choices

This is too niche to support, sorry