bw2 / ConfigArgParse

A drop-in replacement for argparse that allows options to also be set via config files and/or environment variables.
MIT License
728 stars 121 forks source link

Are subcommands supported? #237

Closed wabiloo closed 1 year ago

wabiloo commented 3 years ago

I've been trying to use ConfigArgParse together with sub-commands, but the behaviour is not what I expected...

For example:

import configargparse
import yaml

def main():
    ap = configargparse.ArgParser(
        default_config_files=['/etc/app/conf.d/*.conf', '~/.my_settings', 'defaults.ini'],
        config_file_parser_class=configargparse.ConfigparserConfigFileParser)
    subparsers = ap.add_subparsers(help="sub-command help", parser_class=configargparse.ArgParser, dest="command")
    p0 = subparsers.add_parser('cmd0', help="This is command 0")
    p0.add('-r', help="A random one")
    p = subparsers.add_parser('cmd1', help="This is command 1")
    p.add('-c', '--my-config', required=False, is_config_file=True, help='config file path')
    p.add('-e', '--my-config2', required=False, is_config_file=True, help='config file path')
    p.add('-b', '--bla', required=False, default="DING")
    p.add('--genome', required=False, help='path to genome file')  # this option can be set in a config file because it starts with '--'
    p.add('-v', help='verbose', action='store_true')
    p.add('-d', '--dbsnp', help='known variants .vcf', env_var='DBSNP_PATH')  # this option can be set in a config file because it starts with '--'
    # p.add('--vcf', nargs='+', help='variant file(s)', type=yaml.safe_load)
    # p.add('--vcf2', nargs='+', help='variant file(s)', type=yaml.safe_load)
    g1 = p.add_argument_group("group 1")
    g1.add('--group1_a', help="goup 1 A", env_var='GROUP1_A')

    options = p.parse_known_args()

    print(p.format_help())
    print("----------")
    print(p.format_values())    # useful for logging where different settings came fro
    print("----------")
    print(options)

main()

If I now call it with python3 configargparse_tests.py -h, I get:

usage: configargparse_tests.py cmd1 [-h] [-c MY_CONFIG] [-e MY_CONFIG2]
                                    [-b BLA] [--genome GENOME] [-v] [-d DBSNP]
                                    [--group1_a GROUP1_A]

optional arguments:
  -h, --help            show this help message and exit
  -c MY_CONFIG, --my-config MY_CONFIG
                        config file path
  -e MY_CONFIG2, --my-config2 MY_CONFIG2
                        config file path
  -b BLA, --bla BLA
  --genome GENOME       path to genome file
  -v                    verbose
  -d DBSNP, --dbsnp DBSNP
                        known variants .vcf [env var: DBSNP_PATH]

group 1:
  --group1_a GROUP1_A   goup 1 A [env var: GROUP1_A]

No mention of cmd0 anywhere. If I run with python3 configargparse_tests.py cmd0 -h, I get the exact same output...

bw2 commented 3 years ago

This looks like a bug. A PR to fix this would be appreciated.

ryeleo commented 3 years ago

@wabiloo, I notice that p.parse_known_args() is being called where p is the 2nd subparser, not the top-level parser. I think the intention might be to actually call ap.parse_known_args(), which seems to work as I expect!

The updated src code, using ap.parse_known_args():

import yaml

def main():
    ap = configargparse.ArgParser(
        default_config_files=['/etc/app/conf.d/*.conf', '~/.my_settings', 'defaults.ini'],
        config_file_parser_class=configargparse.ConfigparserConfigFileParser)
    subparsers = ap.add_subparsers(help="sub-command help", parser_class=configargparse.ArgParser, dest="command")
    p0 = subparsers.add_parser('cmd0', help="This is command 0")
    p0.add('-r', help="A random one")
    p1 = subparsers.add_parser('cmd1', help="This is command 1")
    p1.add('-c', '--my-config', required=False, is_config_file=True, help='config file path')
    p1.add('-e', '--my-config2', required=False, is_config_file=True, help='config file path')
    p1.add('-b', '--bla', required=False, default="DING")
    p1.add('--genome', required=False, help='path to genome file')  # this option can be set in a config file because it starts with '--'
    p1.add('-v', help='verbose', action='store_true')
    p1.add('-d', '--dbsnp', help='known variants .vcf', env_var='DBSNP_PATH')  # this option can be set in a config file because it starts with '--'
    # p.add('--vcf', nargs='+', help='variant file(s)', type=yaml.safe_load)
    # p.add('--vcf2', nargs='+', help='variant file(s)', type=yaml.safe_load)
    g1 = p1.add_argument_group("group 1")
    g1.add('--group1_a', help="goup 1 A", env_var='GROUP1_A')

    options = ap.parse_known_args()

    print(p1.format_help())
    print("----------")
    print(p1.format_values())    # useful for logging where different settings came fro
    print("----------")
    print(options)

main()

Output:

usage: test.py [-h] {cmd0,cmd1} ...

positional arguments:
  {cmd0,cmd1}  sub-command help
    cmd0       This is command 0
    cmd1       This is command 1

optional arguments:
  -h, --help   show this help message and exit
wabiloo commented 3 years ago

I stand corrected! Many thanks for that @terminalstderr

jfindlay commented 11 months ago

I get this error output

$ /tmp/venv/bin/python /tmp/conftest.py 
usage: conftest.py cmd1 [-h] [-c MY_CONFIG] [-e MY_CONFIG2] [-b BLA] [--genome GENOME] [-v] [-d DBSNP] [--group1_a GROUP1_A]

options:
  -h, --help            show this help message and exit
  -c MY_CONFIG, --my-config MY_CONFIG
                        config file path
  -e MY_CONFIG2, --my-config2 MY_CONFIG2
                        config file path
  -b BLA, --bla BLA
  --genome GENOME       path to genome file
  -v                    verbose
  -d DBSNP, --dbsnp DBSNP
                        known variants .vcf [env var: DBSNP_PATH]

group 1:
  --group1_a GROUP1_A   goup 1 A [env var: GROUP1_A]

Args that start with '--' can also be set in a config file (specified via -c or -e). Config file syntax allows: key=value, flag=true, stuff=[a,b,c] (for details, see syntax at https://goo.gl/R74nmi). In general, command-line values override environment variables which
override config file values which override defaults.

----------
Traceback (most recent call last):
  File "/tmp/conftest.py", line 32, in <module>
    main()
  File "/tmp/conftest.py", line 27, in main
    print(p1.format_values())    # useful for logging where different settings came fro
  File "/tmp/venv/lib/python3.10/site-packages/configargparse.py", line 1286, in format_values
    for source, settings in self._source_to_settings.items(): #type:ignore[argument-error]
AttributeError: 'ArgumentParser' object has no attribute '_source_to_settings'. Did you mean: 'get_source_to_settings_dict'?

Versions

$ /tmp/venv/bin/pip list
Package        Version
-------------- -------
ConfigArgParse 1.7
pip            22.0.2
PyYAML         6.0.1
setuptools     59.6.0
wheel          0.37.1