Code Monkey home page Code Monkey logo

simpleparsing's Introduction

Build Status PyPI version

Simple, Elegant, Typed Argument Parsing

simple-parsing allows you to transform your ugly argparse scripts into beautifully structured, strongly typed little works of art. This isn't a fancy, complicated new command-line tool either, this simply adds new features to plain-old argparse! Using dataclasses, simple-parsing makes it easier to share and reuse command-line arguments - no more copy pasting!

Supports inheritance, nesting, easy serialization to json/yaml, automatic help strings from comments, and much more!

# examples/demo.py
from dataclasses import dataclass
from simple_parsing import ArgumentParser

parser = ArgumentParser()
parser.add_argument("--foo", type=int, default=123, help="foo help")

@dataclass
class Options:
    """ Help string for this group of command-line arguments """
    log_dir: str                # Help string for a required str argument
    learning_rate: float = 1e-4 # Help string for a float argument

parser.add_arguments(Options, dest="options")

args = parser.parse_args()
print("foo:", args.foo)
print("options:", args.options)
$ python examples/demo.py --log_dir logs --foo 123
foo: 123
options: Options(log_dir='logs', learning_rate=0.0001)
$ python examples/demo.py --help
usage: demo.py [-h] [--foo int] --log_dir str [--learning_rate float]

optional arguments:
  -h, --help            show this help message and exit
  --foo int             foo help (default: 123)

Options ['options']:
   Help string for this group of command-line arguments

  --log_dir str         Help string for a required str argument (default:
                        None)
  --learning_rate float
                        Help string for a float argument (default: 0.0001)

(new) Simplified API:

For a simple use-case, where you only want to parse a single dataclass, you can use the simple_parsing.parse or simple_parsing.parse_known_args functions:

options: Options = simple_parsing.parse(Options)
# or:
options, leftover_args = simple_parsing.parse_known_args(Options)

installation

pip install simple-parsing

API Documentation (Under construction)

Features

  • As developers, we want to make it easy for people coming into our projects to understand how to run them. However, a user-friendly --help message is often hard to write and to maintain, especially as the number of arguments increases.

    With simple-parsing, your arguments and their descriptions are defined in the same place, making your code easier to read, write, and maintain.

  • Modular, Reusable, Cleanly Grouped Arguments

    (no more copy-pasting)

    When you need to add a new group of command-line arguments similar to an existing one, instead of copy-pasting a block of argparse code and renaming variables, you can reuse your argument class, and let the ArgumentParser take care of adding relevant prefixes to the arguments for you:

    parser.add_arguments(Options, dest="train")
    parser.add_arguments(Options, dest="valid")
    args = parser.parse_args()
    train_options: Options = args.train
    valid_options: Options = args.valid
    print(train_options)
    print(valid_options)
    $ python examples/demo.py \
        --train.log_dir "training" \
        --valid.log_dir "validation"
    Options(log_dir='training', learning_rate=0.0001)
    Options(log_dir='validation', learning_rate=0.0001)

    These prefixes can also be set explicitly, or not be used at all. For more info, take a look at the Prefixing Guide

  • It's easy to choose between different argument groups of arguments, with the subgroups function!

  • Default values for command-line arguments can easily be read from many different formats, including json/yaml!

  • Easily save/load configs to json or yaml!.

  • You can easily customize an existing argument class by extending it and adding your own attributes, which helps promote code reuse across projects. For more info, take a look at the inheritance example

  • Dataclasses can be nested within dataclasses, as deep as you need!

  • This is sometimes tricky to do with regular argparse, but simple-parsing makes it a lot easier by using the python's builtin type annotations to automatically convert the values to the right type for you. As an added feature, by using these type annotations, simple-parsing allows you to parse nested lists or tuples, as can be seen in this example

  • (More to come!)

Examples:

Additional examples for all the features mentioned above can be found in the examples folder

simpleparsing's People

Contributors

anivegesana avatar brentyi avatar breuleux avatar conchylicultor avatar diggerk avatar haydenflinner avatar idoby avatar ivanprado avatar jeromepl avatar jessefarebro avatar jonasfrey96 avatar kianmeng avatar lebrice avatar ltluttmann avatar mixilchenko avatar norabelrose avatar rhaps0dy avatar stas00 avatar thewchan avatar tristandeleu avatar wolf1986 avatar yuyu2172 avatar yycho0108 avatar zhiruiluo avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

simpleparsing's Issues

Can I parse nested tuples?

Is your feature request related to a problem? Please describe.

I need to parse nested tuples, as often occur in parameterization -- ie a tuple of (a tuple of names, a tuple of values, a tuple of values ...).

Describe the solution you'd like

To be able to build nested tuples, eg like the field some_tuple_parameterization :

some_tuple_parameterization: Tuple[Tuple[str, str], Tuple[int, float], Tuple[int, float]] = (('foo', 'bar'), (1, 10.98), (-1, 100.01))

or even better, with Ellipsis:

Tuple[Tuple[str, str], Tuple[int, float], ...]

or could be Tuple[Tuple[str,str], Tuple[Tuple[int,float],...]]

More generally, I would like to have container types easily nested.

Describe alternatives you've considered

I've tried the above field, and various permutations, without success

from dataclasses import dataclass
from typing import Tuple
from simple_parsing import ArgumentParser

@dataclass
class Example:
    some_tuple_parameterization: Tuple[Tuple[str, str], Tuple[int, float], Tuple[int, float]] = (('foo', 'bar'), (1, 10.98), (-1, 100.01))

cmds = "--some_tuple_parameterization 'a' 'b' 1 1.1 2 2.001"
parser = ArgumentParser()
parser.add_arguments(Example, "example")
args = parser.parse_args([cmds])
#args = parser.parse_args()
example: Example = args.example
print(example)
expected = "Example(some_tuple_parameterization=(('a','b'),(1,1.1),(2,2.001))"
print(expected)

when run as above:

usage: simple_parser_tuple.py [-h]
                              [--some_tuple_parameterization str str int float int float]
simple_parser_tuple.py: error: unrecognized arguments: --some_tuple_parameterization 'a' 'b' 1 1.1 2 2.001

or when run from the command line ( args = parser.parse_args() ):

$python simple_parser_tuple.py --some_tuple_parameterization 'a' 'b' 1 1.1 2 2.001
usage: simple_parser_tuple.py [-h] [--some_tuple_parameterization str str int float int float] 
simple_parser_tuple.py: error: argument --some_tuple_parameterization: invalid typing.Tuple[typing.Tuple[str, str], typing.Tuple[int, float], typing.Tuple[int, float]] value: 'b'

I also tried this:

MyKeys = Tuple[str, str]
MyValues = Tuple[int, float]

@dataclass
class Example:
    some_tuple_parameterization: Tuple[MyKeys, MyValues, MyValues] = (('foo', 'bar'), (1, 10.98), (-1, 100.01))

with the same result

Additional context
I'm naive regarding python's typing. It may very well be that this functionality is already possible, and that I am just overlooking something. I'm not tied to any particular method of defining types. If there is a solution for nesting general container types, eg using List, then that might work. I prefer Tuple but could convert.

Very nice program by the way. It does just what I was looking for -- allows me to use dataclasses to define my very many parameters in one place, with types and help for command line parsing!

Support for subparser aliases

Is your feature request related to a problem? Please describe.
Naming of subprocess commands is done automatically by introspecting the class name.

Describe the solution you'd like
I'd like to be able to specify one or more aliases, specifically to allow a short version.

Describe alternatives you've considered
I looked into autocomplete libs but these are not reliable or easy to administer.

Add support for optional nested parameter groups

Is your feature request related to a problem? Please describe.
I would like to have a set of parameters that are optional, but if you give one of the set, you must follow the rules for the rest of them. For example, the following code doesn't work - you get asked to provide an input of type B which is impossible from the CLI.

from simple_parsing import ArgumentParser
from dataclasses import dataclass
from typing import *

@dataclass
class A:
    a: str
    b: str
@dataclass
class B:
    a: str
    b: str

@dataclass
class MyCli:
    A: A
    B: Optional[B]



parser = ArgumentParser()
parser.add_arguments(MyCli, dest="args")
args = parser.parse_args()

Gives help output:

usage: scratch_1.py [-h] [-B B] -a str -b str
scratch_1.py: error: the following arguments are required: -a/--a, -b/--b

Describe the solution you'd like
In the above example, I would like A (and all it's sub-parameters) to be mandatory. And I would like B to be optional; but if B.a is given, then B.b is also mandatory.

Another way to explain this is that B is a feature flag, where if that flag is given then you must also provide a set of parameters for that feature.

Describe alternatives you've considered
Alternative is that I make all parameters of B optional, and validate myself in a __post_init__ or similar to re-create the behaviour of all being mandatory if any one is given. But it seems that SimpleParsing is the right place to solve this, rather than require the user to do such a workaround.

List[int] parsed as a single value

Describe the bug
List[int] parsed as a single value.

To Reproduce

from dataclasses import dataclass, field
from simple_parsing import ArgumentParser
from typing import List


@dataclass
class Opts:
    int_list: List[int] = field(default_factory=[1,2].copy)


parser = ArgumentParser()
parser.add_arguments(Opts, dest="opts")

def test(arg_string):
    args = parser.parse_args(arg_string.split())
    opts: Opts = args.opts
    print(opts)
    print(type(opts.int_list))

# This gives an expected result:
#
#     Opts(int_list=[1, 2])
#     <class 'list'>

test("")


# This fails with: 
#
#     usage: test_list_single.py [-h] [--int_list int]
#     test_list_single.py: error: argument --int_list: invalid int value: '[1,2]'

test("--int_list [1,2]")

Expected behavior
A clear and concise description of what you expected to happen.

$ python test_list_single.py
Opts(int_list=[1, 2])
<class 'list'>
Opts(int_list=[1, 2])
<class 'list'>

Actual behavior

$ python test_list_single.py
Opts(int_list=[1, 2])
<class 'list'>
usage: test_list_single.py [-h] [--int_list int]
test_list_single.py: error: argument --int_list: invalid int value: '[1,2]'

Desktop (please complete the following information):

  • Version 0.0.15.post1
  • Python version: 3.9.2

Additional context
The parsing works correct if I add second instance of the Opts to the arguments and use ConflictResolution.ALWAYS_MERGE:

from dataclasses import dataclass, field
from simple_parsing import ArgumentParser, ConflictResolution
from simple_parsing.helpers import list_field
from typing import List


@dataclass
class Opts:
    int_list: List[int] = field(default_factory=[1,2].copy)


parser = ArgumentParser(conflict_resolution=ConflictResolution.ALWAYS_MERGE)
parser.add_arguments(Opts, dest="opts1", default=Opts())
parser.add_arguments(Opts, dest="opts2", default=Opts())

# print(parser.equivalent_argparse_code())

def test(arg_string):
    args = parser.parse_args(arg_string.split())
    opts: Opts = args.opts1
    print(opts)
    print(type(opts.int_list))

# This gives an expected result:
#
#     Opts(int_list=[1, 2])
#     <class 'list'>

test("")


# This gives an expected result:
#
#     Opts(int_list=[1, 2])
#     <class 'list'>

test("--int_list [1,2]")

`wrappers` is missing when installing using pip

Describe the bug
I tried to install using pip. When trying to import I get this:

% python
Python 3.7.4 (default, Aug 13 2019, 20:35:49) 
[GCC 7.3.0] :: Anaconda, Inc. on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import simple_parsing
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/serdyuk/miniconda3/lib/python3.7/site-packages/simple_parsing/__init__.py", line 5, in <module>
    from .parsing import ArgumentParser, ConflictResolution
  File "/home/serdyuk/miniconda3/lib/python3.7/site-packages/simple_parsing/parsing.py", line 23, in <module>
    from .wrappers import DataclassWrapper, FieldWrapper
ModuleNotFoundError: No module named 'simple_parsing.wrappers'
>>> 

To Reproduce

  1. pip install simple_parsing
  2. `python -c "import simple-parsing"

Expected behavior
It doesn't crash.

Desktop (please complete the following information):

  • OS: Ubuntu 19.04
  • miniconda 3
  • python 3.7.4

Show possible values of a list field, if the amount of values is finite

Is your feature request related to a problem? Please describe.
I'd like to be able to pass a list of flags to my program. The flags are all listed in an enum and the variable in the dataclass is defined as flags: List[MyEnum] = ...
Argument parsing works, but no proper help text is generated.

Describe the solution you'd like
If the argument of the List annotation has a finite amount of possible values (e.g. enum values), it would be nice to list those. This is already done, when a field is directly annotated as an enum.

Possible output:

  --my_config [MyEnum [MyEnum ...]]
                        Configure some values here
                        (default: [SomeValue, OneMore])
                        (possible: {SomeValue, AnotherValue, YetAnotherValue, OneMore})

Although I'm not sure, where/how the values should be listed

Describe alternatives you've considered
I could manually update the help text when the enum values change, but that violates the DRY principle.

Confusing command line options for bool's with `default=True`

Having a boolean option with a default value of True:

from dataclasses import dataclass

from simple_parsing import ArgumentParser


@dataclass
class Args:
    enable_water: bool = True  # Do you want water?


parser = ArgumentParser()
parser.add_arguments(Args, dest="args")

print(parser.parse_args(["--enable_water"]))
# => Namespace(args=Args(enable_water=False))

Creates this very confusing behaviour that passing the option (without a boolean value) disables it, so passing enable_water actually causes enable_water to be set to False, which is not very intuitive.

Even though this behaviour corresponds to store_false in the classic python ArgumentParser, it would be nicer to make this more clear. For example by changing the command line option automatically to --no_enable_water (by prepending a no_) when the default is False, and then setting enable_water to False when the option --no_enable_water is passed.

Breaks with `from __future__ import annotations`

When using from __future__ import annotations at the top of any of the examples, SimpleParsing crashes with:

...
  File "/Users/jrueegg/miniconda/envs/shrek/lib/python3.7/site-packages/simple_parsing/parsing.py", line 221, in _preprocessing
    wrapper.add_arguments(parser=self)
  File "/Users/jrueegg/miniconda/envs/shrek/lib/python3.7/site-packages/simple_parsing/wrappers/dataclass_wrapper.py", line 103, in add_arguments
    group.add_argument(*wrapped_field.option_strings, **wrapped_field.arg_options)
  File "/Users/jrueegg/miniconda/envs/shrek/lib/python3.7/argparse.py", line 1364, in add_argument
    raise ValueError('%r is not callable' % (type_func,))
ValueError: 'str' is not callable

I guess you're missing a typing.get_type_hints() somewhere in the code...

Small help presentation changes

Is your feature request related to a problem? Please describe.
Two minor things I would like to tweak in the help:
1, I'd like to remove the call-outs to what dataclass is being populated, in favor of these not being exposed.
2, I'd like to display "-"s instead of "_"s, because I don't want my users to think they have to hold shift between letters when typing argument names ๐Ÿ˜„

usage: my-prog my_cmd [-h] --path str [--int_var int]

optional arguments:
  -h, --help     show this help message and exit

MyCmdArgs ['dataclass_arg']:
  MyCmdArgs(path:str, freq_ms:int=1000)

  --path str
  --int_var int  (default: 1000)

would become

usage: my-prog my_cmd [-h] --path str [--int-var int]

optional arguments:
  -h, --help     show this help message and exit
  --path str
  --int-var int  (default: 1000)

I poked around help_formatter.py but it's not obvious to me where this little section with the constructor of the class's signature is coming from. Could probably find by spending time later in a debugger. It is somewhat obvious where the "_"'s in the help come from, and I will check later if it's trivial to add an option to switch these to "-"'s as a user of the lib.

Thank you!

Simple parsing with nesting breaks line_profiler (kernprof)

Describe the bug
When calling a script with kernprof (line_profiler), it breaks if simple parsing uses nesting.
There is no issue if nesting is not used.

To Reproduce

tmp.py

from dataclasses import dataclass
from time import sleep
from simple_parsing import ArgumentParser

@dataclass
class NestedArgs():
    nested1: str = 'nested1'

@dataclass
class Opts():
    nested: NestedArgs  # when this variable is commented out, then the profiling is successful
    arg1: bool = False
    arg2: int = 4

@profile
def func_to_line_prof():
    sleep(0.01)
    sleep(1)
    sleep(3)

def main():
    parser = ArgumentParser()
    parser.add_arguments(Opts, dest='cfg')
    args = parser.parse_args().cfg
    func_to_line_prof()


if __name__ == '__main__':
    main()

On commandline (bash):

pip install line_profiler
kernprof -v -l tmp.py

Expected behavior
Script will run and func_to_line_prof() will get profiled.

rote profile results to tmp.py.lprof
Timer unit: 1e-06 s

Total time: 4.01383 s
File: tmp.py
Function: func_to_line_prof at line 16

Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================
    16                                           @profile
    17                                           def func_to_line_prof():
    18         1      10070.0  10070.0      0.3      sleep(0.01)
    19         1    1001030.0 1001030.0     24.9      sleep(1)
    20         1    3002727.0 3002727.0     74.8      sleep(3)

Actual behavior
An exception is raised

raceback (most recent call last):                                                                                                           
  File "/home/yatzmon/anaconda3/envs/rlgpu/bin/kernprof", line 8, in <module>                                                                
    sys.exit(main())                                                                                                                         
  File "/home/yatzmon/anaconda3/envs/rlgpu/lib/python3.7/site-packages/kernprof.py", line 234, in main                                       
    execfile(script_file, ns, ns)                                                                                                            
  File "/home/yatzmon/anaconda3/envs/rlgpu/lib/python3.7/site-packages/kernprof.py", line 39, in execfile
    exec_(compile(f.read(), filename, 'exec'), globals, locals)
  File "tmp.py", line 30, in <module>
    main()
  File "tmp.py", line 25, in main
    args = parser.parse_args().cfg
  File "/home/yatzmon/anaconda3/envs/rlgpu/lib/python3.7/argparse.py", line 1755, in parse_args
    args, argv = self.parse_known_args(args, namespace)
  File "/home/yatzmon/anaconda3/envs/rlgpu/lib/python3.7/site-packages/simple_parsing/parsing.py", line 162, in parse_known_args
    self._preprocessing()
  File "/home/yatzmon/anaconda3/envs/rlgpu/lib/python3.7/site-packages/simple_parsing/parsing.py", line 221, in _preprocessing
    wrapper.add_arguments(parser=self)
  File "/home/yatzmon/anaconda3/envs/rlgpu/lib/python3.7/site-packages/simple_parsing/wrappers/dataclass_wrapper.py", line 90, in add_arguments
    group = parser.add_argument_group(title=self.title, description=self.description)
  File "/home/yatzmon/anaconda3/envs/rlgpu/lib/python3.7/site-packages/simple_parsing/wrappers/dataclass_wrapper.py", line 178, in description
    doc = docstring.get_attribute_docstring(self.parent.dataclass, self._field.name)            
  File "/home/yatzmon/anaconda3/envs/rlgpu/lib/python3.7/site-packages/simple_parsing/docstring.py", line 38, in get_attribute_docstring
    source = inspect.getsource(some_dataclass)
  File "/home/yatzmon/anaconda3/envs/rlgpu/lib/python3.7/inspect.py", line 973, in getsource
    lines, lnum = getsourcelines(object)
  File "/home/yatzmon/anaconda3/envs/rlgpu/lib/python3.7/inspect.py", line 955, in getsourcelines
    lines, lnum = findsource(object)
  File "/home/yatzmon/anaconda3/envs/rlgpu/lib/python3.7/inspect.py", line 812, in findsource
    raise OSError('could not find class definition')
OSError: could not find class definition

Desktop (please complete the following information):

  • Couldn't find it
  • Python version: 3.7.7

Consider using typing_inspect, to reduce overhead in maintaining own implementation

I'm currently working on a project that also makes heavy use of type hints.
From what I have seen, the internal API of the typing module is still changed quite often.
One of the maintainers of the typing module also has a neat library which provides a more stable API to the typing internals.
I suggest to take a look at typing_inspect to avoid accessing internal fields like __args__ and __origin__ directly.
This should reduce implementation overhead and make the library more stable when new python versions are released.

Add a way to ignore a field, without having to set `init=False`

Is your feature request related to a problem? Please describe.
Would be nice to have a way to tell SimpleParsing to simply ignore a field, (i.e., to not try to parse it via command-line.)

Describe the solution you'd like
Something simple, elegant, and easy to implement.

Describe alternatives you've considered

  • Adding a ignore=True/cmd=False kwarg to field()
  • (bad idea) detecting some flag/specific strings in the field 'docstring'

Additional context
This would be very helpful when trying to save/load state to files, for example. Your dataclass might have some command-line arguments as well as some attributes that are used to keep some kind of state.
If field(init=False) is used, then the dataclasses.asdict(...) function doesn't return those fields, and they can't be passed to the constructor when creating them from a dictionary (read from a JSON file, for example)

Support for Positional Arguments

Is your feature request related to a problem? Please describe.
It would be nice to be able to have positional arguments, especially for cmds that only take one argument. For example, given the following cmd, it would be annoying to always have to instead type:
cat myfile | sed 's/abc/123/g'
cat --file myfile | sed --pattern 's/abc/123/g'.

Describe the solution you'd like
I think I just need a way to tell 'options_strings' not to add the prefix dashes. If the option name is specified to argparse without dashes, it is positional by default.

Optional List/Tuple only accepts first value

Describe the bug
Specifying a List or Tuple field as optional only accepts the first value.

To Reproduce

from dataclasses import dataclass
from typing import Optional, Tuple
from simple_parsing import ArgumentParser

@dataclass
class MyConfig:
    values: Optional[Tuple[int, int]]

parser = ArgumentParser()
parser.add_arguments(MyConfig, dest="cfg")
args_none, _ = parser.parse_known_args([])
args_values, extra = parser.parse_known_args(["--values", "3", "4"])

Expected behavior
args_values.cfg.values should be the tuple (3,4) and extra should be []
But instead args_values.cfg.values is 3 and extra is ['4']

Additionally there should be parse error if the number of items for a fixed-length tuple does not match the definition.
IE, specifying too few or too many values should generate an error if the tuple is of a fixed length.

'set' type seems to not be properly supported

Describe the bug
The 'set' type does not seem to be properly supported.

To Reproduce
Reproduction code: test.py

#!/usr/bin/env python3
from dataclasses import dataclass
from simple_parsing import ArgumentParser

parser = ArgumentParser()

@dataclass
class Options:
    set_things: set[str]
    list_things: list[str]

parser.add_arguments(Options, dest="options")

args = parser.parse_args()
print("options:", args.options)

Reproduction invocation:

$ ./test.py --list_things a b a --set_things a b a

Expected behavior

$ ./test.py --list_things a b a --set_things a b a
options: Options(set_things={'a', 'b'}, list_things=['a', 'b', 'a'])`

Actual behavior

$ ./test.py --list_things a b a --set_things a b a
usage: test.py [-h] --set_things set --list_things list
test.py: error: unrecognized arguments: b a

Desktop (please complete the following information):

  • Python version: 3.9.7

Additional context
If you only pass in a single value to --set_things, it properly creates a set from it.

I can't imagine that supporting this would be hard given the current list() support... and while I could simply take a list and create a set from it, that feels completely unnecessary.

Allowing a float arg to get an int value (according to PEP 3141)

PEP 3141 defines Python's numeric tower... when an argument is annotated as having type float, an argument of type int is acceptable; similar, for an argument annotated as having type complex, arguments of type float or int are acceptable.

Can you please make simple-parsing behave according to PEP 3141, or at least accept int values also for args whose type hint in the dataclass is float? Much more intuitive...

Thanks, you're awesome!

default values passed to `mutable_field` aren't used properly

Describe the bug
A clear and concise description of what the bug is.

To Reproduce

from simple_parsing import ArgumentParser
from dataclasses import dataclass
from simple_parsing.helpers import mutable_field


@dataclass
class Foo:
    bar: int = 123


@dataclass
class Baz:
    a_foo: Foo = mutable_field(Foo, bar=111)
    b_foo: Foo = mutable_field(Foo, bar=222)


if __name__ == "__main__":
    parser = ArgumentParser()
    parser.add_arguments(Baz, "baz")
    args = parser.parse_args()
    a_foo: Foo = args.baz.a_foo
    b_foo: Foo = args.baz.b_foo
    print(a_foo.bar, b_foo.bar)

Expected behavior
A clear and concise description of what you expected to happen.

$ python issue.py
111 222

Actual behavior
A clear and concise description of what is happening.

$ python issue.py
123 123

Nesting has confusing, hidden, behaviour

Describe the bug
It took me a lot of experimentation to find out that parameters are only prefixed with their nested group name if the name of the nested parameter conflicts with another. I expected it to prefix it with the nested group name at all times, and I think it should do this.

It should do this because of the following situation:

  • I have a dataclass of parameters I want to re-use in multiple places (lets call it A, with parameters a and b)
  • If I use it in one place which only has a single nested group of type A, then I specify arguments as --a asdf and --b asdf.
  • If I use it in a second place which has multiple nested groups then if those groups also specify parameters of a and b then suddenly my API has silently changed from --a asdf to --A.a asdf without touching that bit of code

There is a second situation where this is useful as well:
I define a dataclass with parameter names that make sense in the context of the name of the class they are defined under. However, if you take those names out of context without the name of the containing class, then they are ill-defined and therefore the resulting CLI is confusing. e.g.

@dataclass
class Bananas:
    count: int
    
@dataclass
class MyCli:
    bananas: Bananas

Gives the following CLI helptext:

usage: scratch_1.py [-h] --count int
scratch_1.py: error: the following arguments are required: --count

Note that count is now completely meaningless. I can obviously add a docstring to explain it, but that means that every parameter docstring in Bananas must make sure it references Bananas.

To Reproduce
Comment/uncomment out B from MyCli and see the output of --help change.

from simple_parsing import ArgumentParser
from dataclasses import dataclass
from typing import *

@dataclass
class A:
    a: str
    b: str
@dataclass
class B:
    a: str
    b: str

@dataclass
class MyCli:

    A: A
    B: B  # Comment/uncomment this.

parser = ArgumentParser()
parser.add_arguments(MyCli, dest="args")
args = parser.parse_args()

Expected behavior
Subgroups should always include the group name when specifying their parameters.

I.e. in the above example, without B, you should get:

usage: scratch_1.py [-h] -a str -b str
scratch_1.py: error: the following arguments are required: -A.a/--A.a, -A.b/--A.b

whereas you currently get this:

usage: scratch_1.py [-h] -a str -b str
scratch_1.py: error: the following arguments are required: -a/--a, -b/--b

Specifying help text on a field causes the help to indicate a default of None even if there is no default

Describe the bug
When help text is added to a field that does not have a default value, the help text indicates a default value of None when it should not.

To Reproduce

#!/usr/bin/env python3
# test_simple.py
from dataclasses import dataclass
from simple_parsing import ArgumentParser

@dataclass
class Options:
    list_items: list[str]  # SOMETHING

parser = ArgumentParser(add_option_string_dash_variants=True)
parser.add_arguments(Options, dest="options")
args=parser.parse_args()
print(args.options)

Expected behavior
I expected no default to be indicated, as below:

$ ./test_simple.py --help
usage: test_simple.py [-h] --list-items list

optional arguments:
  -h, --help            show this help message and exit

Options ['options']:
  Options(list_items: list)

  --list-items list, --list_items list
                        SOMETHING

Actual behavior
A default is indicated even though there is no default because the parameter is simply required.

$ ./test_simple.py --help
usage: test_simple.py [-h] --list-items list

optional arguments:
  -h, --help            show this help message and exit

Options ['options']:
  Options(list_items: list)

  --list-items list, --list_items list
                        SOMETHING (default: None)

Desktop (please complete the following information):

  • Python version: 3.9.7

Help does not appear if using metadata in fields

Describe the bug
Hi, it seems, that when the dataclass has metadata defined through field (from simple_parsing, of course), the help in form of comments does not get picked up.

To Reproduce

from simple_parsing import ArgumentParser, field
from dataclasses import dataclass

@dataclass
class InputArgs:
    # Start date from which to collect data about base users. Input in iso format (YYYY-MM-DD).
    # The date is included in the data
    start_date: str = field(alias="s", metadata={'a':'b'})

    # End date for collecting base users. Input in iso format (YYYY-MM-DD). The date is included in the data.
    # Should not be before `start_date`
    end_date: str = field(alias="e")

def parse_args() -> InputArgs:
    parser = ArgumentParser("Prepare input data for training")
    parser.add_arguments(InputArgs, dest="args")

    args = parser.parse_args()
    return args.args

if __name__ == "__main__":
    parsed_args = parse_args()
    print(parsed_args)

python test.py -h

Output

usage: Prepare input data for training [-h] -s str -e str

optional arguments:
  -h, --help            show this help message and exit

InputArgs ['args']:
  InputArgs(start_date: str, end_date: str)

  -s str, --start_date str
  -e str, --end_date str
                        Start date from which to collect data about base
                        users. Input in iso format (YYYY-MM-DD). The date is
                        included in the data End date for collecting base
                        users. Input in iso format (YYYY-MM-DD). The date is
                        included in the data. Should not be before
                        `start_date` (default: None)

Expected output:

usage: Prepare input data for training [-h] -s str -e str

optional arguments:
  -h, --help            show this help message and exit

InputArgs ['args']:
  InputArgs(start_date: str, end_date: str)

  -s str, --start_date str
                        Start date from which to collect data about base
                        users. Input in iso format (YYYY-MM-DD). The date is
                        included in the data (default: None)
  -e str, --end_date str
                        End date for collecting base users. Input in iso
                        format (YYYY-MM-DD). The date is included in the data.
                        Should not be before `start_date` (default: None)

Nested parameter name with dataclass

Hi, great library, thanks !
I have an issue with nested parser from dataclass.

When trying to create a parser from a dataclass


@dataclass
class JBuildRelease:
    id: int 
    url: str  
    docker_image: str  

    parser = simple_parsing.ArgumentParser()
    parser.add_argument('--run_id', type=str)
    parser.add_arguments(JBuildRelease, dest='jbuild', prefix='jbuild')
    args = parser.parse_args()

I get the following:

usage: release.py [-h] [--run_id str] --id int --url str --docker_image str
release.py: error: the following arguments are required: --id, --url, --docker_image

I wish the arguments would be prefixed like so: --jbuild.id 1 --jbuild.url url --jbuild.docker_image image
Is this flow supported ?

Subparsers "steal" parsing from the parent

Describe the bug
Hi, thanks for the amazing work!

I am honestly not sure, whether this should be classified as a bug or not, because it is a default behaviour of argparse, however, there are workarounds.

What happens is that whenever parser encounters a sub command, it moves all the parsing to this subparser and does not return back, meaning, that any options from the parent parser are invalid after any subparser command. Which as I said, is a "standard" behaviour of argparse, but I am convinced it is wrong and it would be clearer if it was circumvented.

To Reproduce
Steps to reproduce the behavior:

  1. Use example from the examples/subparsers and save as tmp.py
from dataclasses import dataclass
from typing import *
from pathlib import Path
from simple_parsing import ArgumentParser, subparsers

@dataclass
class Train:
    """Example of a command to start a Training run."""
    # the training directory
    train_dir: Path = Path("~/train")

    def execute(self):
        print(f"Training in directory {self.train_dir}")


@dataclass
class Test:
    """Example of a command to start a Test run."""
    # the testing directory
    test_dir: Path = Path("~/train")

    def execute(self):
        print(f"Testing in directory {self.test_dir}")


@dataclass
class Program:
    """Some top-level command"""
    command: Union[Train, Test]
    verbose: bool = False   # log additional messages in the console.
    
    def execute(self):
        print(f"Executing Program (verbose: {self.verbose})")
        return self.command.execute()


parser = ArgumentParser()
parser.add_arguments(Program, dest="prog")
args = parser.parse_args()
prog: Program = args.prog

print("prog:", prog)
prog.execute()
  1. Try to run it as python tmp.py train --verbose
usage: tmp.py [-h] [--verbose bool] {train,test} ...
tmp.py: error: unrecognized arguments: --verbose

Expected behavior
I expect to be able to parse optional arguments from parent dataclass even after specifying subcommand, i.e. python tmp.py train --verbose == python tmp.py --verbose train, since verbose is optional.

Additional context
It is possible to circumventing it by creating "temporary" parser for all the parent options and give this temporary parser as parent_parsers argument to the created subparsers, however, it is not possible to cleanly do with simple-parsing, it would have to be implemented on simple-parsing level

Representing nested args by their full "nesting path" even with no conflicts

simple-parsing is simply awesome (thanks!). I'd like to suggest a feature that is crucial for parsing nested args, in my opinion, and the support of other researchers in my group. Here is a minimal example to make things clear:

from dataclasses import dataclass
from simple_parsing import ArgumentParser, ConflictResolution

@dataclass
class WandbArgs:
    dir: str = ''  # Wandb run cache dir, relative to project_path; leave empty for default
    use: bool # Whether to use wandb for experiment control, logging, saving...

@dataclass
class Data:
    dir: str = ''  # Data dir, relative to project_path; leave empty for default

@dataclass
class ProjectArgs:
    data: Data = Data()
    wandb: WandbArgs = WandbArgs()


parser = ArgumentParser(conflict_resolution=ConflictResolution.EXPLICIT)
parser.add_arguments(ProjectArgs, dest='args')
args = parser.parse_args().args

A command line to set all values can be: --use=True --wandb.dir='wandb' --data.dir='data'.

Since I passed ConflictResolution.EXPLICIT:

  • Nested args with conflicts - args that appear under different dataclasses, dir in this example, are allowed and accessed in command line by their full "nesting path", wandb.dir and data.dir. This is organized and makes sense.
  • HOWEVER, unique args (no conflicts) appear without their "nesting path", in this example use.

I claim that having wandb.use in the command line is much more clear, unambiguous, simple and intuitive than use (current behavior), especially for those who have many args and invested time in organizing them with nesting, to make it organized, clear and simple!.

I found a hack to get this functionality: I force duplication for all args by adding parser.add_arguments(ProjectArgs, dest='duplicated') to my example above (and ignoring the duplicated args) - forcing all args to apepar in command line with their full path. However, since I have many args in my current project, I need to also download and modify simple-parsing source code - set max_attempts=200 in simple_parsing -> conflicts.py - otherwise it raises an error, assuming so many conflicts are not possible and must be an error.

Therefore I suggest to add a force_explicit argument to ArgumentParser, such that ArgumentParser(force_explicit=True) makes all nested args appear in command line with their full "nesting path", no hack, no ConflictResolution.EXPLICIT, no max number of conflicts - simple, organized and clear!

Thanks!

Tuple parsing is incorrect - results in nested tuples

Describe the bug
This dataclass:

@dataclass
class MyCli:
    asdf: Tuple[str, ...]

gets parsed as:
MyCli(asdf=(('asdf',), ('fgfh',)))
but it should be:
MyCli(asdf=('asdf', 'fgfh'))

This works correctly with List instead of Tuple however.

To Reproduce
Run the following with arguments --asdf asdf fgfh

from simple_parsing import ArgumentParser
from dataclasses import dataclass
from typing import *


@dataclass
class MyCli:
    asdf: Tuple[str, ...]

parser = ArgumentParser()
parser.add_arguments(MyCli, dest="args")
args = parser.parse_args()
print(args.args)  # MyCli(asdf=(('asdf',), ('fgfh',)))
print(args.args.asdf)  # (('asdf',), ('fgfh',))
print(args.args.asdf[0])  # ('asdf',)

Expected behavior
Should not result in each element of the tuple also being a single-element tuple.

Default not displayed in the help text when no description comment exists

This example:

from dataclasses import dataclass

from simple_parsing import ArgumentParser

parser = ArgumentParser()


@dataclass
class Options:
    """These are the options"""

    foo: str = "aaa"  # Description
    bar: str = "bbb"


parser.add_arguments(Options, dest="options")

args = parser.parse_args(["--help"])

outputs:

usage: scratch.py [-h] [--foo str] [--bar str]

optional arguments:
  -h, --help  show this help message and exit

Options ['options']:
  These are the options

  --foo str   Description (default: aaa)
  --bar str

Why is the default value displayed for foo, but not for bar? Even when no description comment is given, it would be useful to know the default value set, I would think?

date, datetime support

How to use dates with simple parsing

Since python builtins dt.date and dt.datetime do not support string parsing in constructor, the only way to use dates now is pd.Timestamp class

from dataclasses import dataclass
import pandas as pd
from simple_parsing import ArgumentParser

@dataclass
class A:
    date: pd.Timestamp

parser = ArgumentParser()
parser.add_arguments(A, 'a')
assert parser.parse_args(['--date', '20210924']).a == A(pd.Timestamp('20210924'))

But someone doesn't want to install pandas as dependency and dates support could be helpful

Datetimes can be specified in different formats:

  • %Y%m%d
  • %Y-%m-%d
  • %Y-%d-%m
    So basic usage can be
@dataclass
class A:
    date: dt.date = date_field(fmt='%Y%m%d')
    datetime: dt.datetime = date_field(fmt='%Y%m%d %H%m%s')

Some problems and possible solutions

  1. dt.date has no strptime method

postprocessor should check that type is not dt.datetime subclass and do something like

dt.datetime.strptime(raw_data, metadata.get('fmt', '%Y-%m-%d)).date()
  1. Datetime is often used with timezones and strptime doesn't handle them correctly. Also timezones have different implementations and which one to use during postprocessing
>>> import datetime as dt
>>> import zoneinfo  # see python3.9 or zone info backport
>>> dt.datetime(2021, 9, 24, 5, 0, 0, tzinfo=zoneinfo.ZoneInfo('Europe/Moscow')).isoformat()
'2021-09-24T05:00:00+03:00'
>>> dt.datetime.fromisoformat('2021-09-24T05:00:00+03:00')
datetime.datetime(2021, 9, 24, 5, 0, tzinfo=datetime.timezone(dt.datedelta(seconds=10800)))

You can see that named timezone and timezone with shift are not the same things since there are lots of regions with daylight saving time.
This problem can be solved with custom postprocessor (see below)

  1. There are lots of dt.date and dt.datetime subclasses (eg pendulum and arrow)

pendulum and arrow libraries have their parse and get methods respectively to parse strings.
We can implement optional argument postprocessor of type Optional[Callable[[str], T]] for field. This would provide vast range of possibilities for simple_parsing users.
The basic usage can be

@dataclass
class A:
    date: dt.date = field(postprocessor=lambda x: dt.datetime.strptime(x, '%Y%m%d').date())
    pdatetime: pendulum.DateTime = field(postprocessor=pendulum.parse)

Support Optional to allow truly optional arguments

Is your feature request related to a problem? Please describe.
I'm creating a tool that can be seeded. But if no seed is given, then i will just take a random seed.
The problem that I'm currently facing is that I can only define the seed as int and therefore cannot set it to None as default.

class Config:
    seed: int = 42

Describe the solution you'd like
I would like to be able to use something like this:

class Config:
    seed: Optional[int] = None

But this crashes the help generation

Describe alternatives you've considered
My current workaround is to use a certain magic value for the seed to indicate that it was not defined via the command line arguments.

Also, really nice library ๐Ÿ‘

Bug when parsing N lists of N elements each, without a default value passed to `add_arguments`

Describe the bug
OK so this one is tricky and very specific, and shouldn't impact anyone really.
When we're trying to parse multiple lists using the same attribute (no prefixing, aka ConflictResolution.ALWAYS_MERGE mode),
When the list length and the number of instances to parse is the same, AND there is no default value passed to add_arguments, it gets parsed as multiple lists each with only one element, rather than duplicating the field's default value correctly.

For example:

from dataclasses import dataclass, field
from typing import List, Tuple

from simple_parsing import ArgumentParser, ConflictResolution

@dataclass
class CNNStack():
    name: str = "stack"
    num_layers: int = 3
    kernel_sizes: Tuple[int,int,int] = (7, 5, 5)
    num_filters: List[int] = field(default_factory=[32, 64, 64].copy)

parser = ArgumentParser(conflict_resolution=ConflictResolution.ALWAYS_MERGE)

num_stacks = 3
for i in range(num_stacks):
    parser.add_arguments(CNNStack, dest=f"stack_{i}")

# Parse with no arguments (should get all default values)
args = parser.parse_args()
stack_0 = args.stack_0
stack_1 = args.stack_1
stack_2 = args.stack_2

print(stack_0, stack_1, stack_2, sep="\n")
expected = """
CNNStack(name='stack', num_layers=3, kernel_sizes=(7, 5, 5), num_filters=[32, 64, 64])
CNNStack(name='stack', num_layers=3, kernel_sizes=(7, 5, 5), num_filters=[32, 64, 64])
CNNStack(name='stack', num_layers=3, kernel_sizes=(7, 5, 5), num_filters=[32, 64, 64])
"""
actual = """
CNNStack(name='stack', num_layers=3, kernel_sizes=(7, 5, 5), num_filters=32)
CNNStack(name='stack', num_layers=3, kernel_sizes=(7, 5, 5), num_filters=64)
CNNStack(name='stack', num_layers=3, kernel_sizes=(7, 5, 5), num_filters=64)
"""

Exit Code 0 when program doesn't successfully run

When I create an argparser and I have provided too many/too few parameters, the exit code is still 0, even though the program hasn't been executed properly. For example,

from simple_parsing import ArgumentParser, ConflictResolution
parser = ArgumentParser(conflict_resolution=ConflictResolution.EXPLICIT)
parser.add_argument("input_1")
parser.add_argument("input_2")
args = parser.parse_args()

but when I run

python3 main.py asdf

the error code

echo $?

still returns 0, even though I hadn't provided enough variables and the overall program failed to run

Hangup when trying to call `parse_args` second time

Describe the bug
Script hangs up when trying to call parser.parse_args() two times. It doesn't usually happen in real code, but I think exception would be better than hangup.

To Reproduce

from dataclasses import dataclass
import simple_parsing

parser = simple_parsing.ArgumentParser()
parser.add_argument("--foo", type=int, default=123, help="foo help string")

@dataclass
class Options:
    """ Help string for this group of command-line arguments """
    log_dir: str                # Help string for a required str argument    
    learning_rate: float = 1e-4 # Help string for a float argument

parser.add_arguments(Options, dest="options")

args = parser.parse_args("--log_dir logs --foo 123".split())
args = parser.parse_args("--log_dir logs --foo 123".split())

Expected behavior
Does not hang up.

Add support for a config file with optional overriding

Is your feature request related to a problem? Please describe.
I would like to be able to load the args configuration from a json/yaml file, and at the same time override some values from the command line

Describe the solution you'd like
e.g.
python my_script.py --config=conf.yaml --lr=1e-2

Describe alternatives you've considered
parsing the commandline argv for the overriding args - and manually set them - however this makes a lot it difficult - when dataclasses are nested

disable help for some arguments

Is your feature request related to a problem? Please describe.
Many times I wouldn't like to expose all the dataclass arguments in the help, but I'd still want to have them documented in the source code.
Would it be possible to disable help for those arguments? maybe by a prefix like #nohelp in their comment

Describe the solution you'd like
If an argument comment starts with #nohelp then this argument would not be exposed in the help

Describe alternatives you've considered
Define those arguments in __post_init__()

Detect conflicts between 'regular' and 'field' arguments

Describe the bug
SimpleParsing doesn't currently account for conflict in option strings between "regular" arguments (added through parser.add_argument("--foo", default=123) and through parser.add_argument(DataclassWithFooField, dest="bar").

To Reproduce
(modified example from #46 )

@dataclass
class JBuildRelease:
    id: int 
    url: str  
    docker_image: str  
 
 if __name__ == "__main__":
    parser = simple_parsing.ArgumentParser()
    parser.add_argument('--id', type=str)
    parser.add_arguments(JBuildRelease, dest="jbuild")
    args = parser.parse_args() # <--- This throws an error, conflicting option strings

Expected behavior
Adds changes the option string of the id field from "--id" to "--jbuild.id".

Parsing Multiple Instances with List Attribute Throws Error

Describe the bug
When parsing multiple instances, each of them having a List attribute with a default value, there shouldn't be an error message, each instance should instead get the default list.

To Reproduce
Steps to reproduce the behavior:

$ python examples/list_example.py 
Traceback (most recent call last):
  File "examples/list_example.py", line 22, in <module>
    cnn_stack = CNNStack.from_args_multiple(args, 2)
  File "/home/fabrice/Source/SimpleParsing/simple_parsing/parsing.py", line 207, in from_args_multiple
    f"The field '{field_name}' contains {len(field_values)} values, but either 1 or {num_instances_to_parse} values were expected.")
simple_parsing.parsing.InconsistentArgumentError: The field 'kernel_sizes' contains 3 values, but either 1 or 2 values were expected.

Expected behavior

[
CNNStack(name='stack', num_layers=3, kernel_sizes=[7,5,5], num_filters=[32, 64, 64]), CNNStack(name='stack', num_layers=3, kernel_sizes=[7,5,5], num_filters=[32, 64, 64])
]

pyyaml dependency missing in setup.py

Not sure if this is the intended behavior (and viewed as an optional install), but since pyyaml isn't marked as a dependency, in setup.py, calling .save("...yaml") or .save_yaml on a Serializable data class will crash, due to the missing package. If it is intended to be optional, perhaps it's a good idea to mark it as such in the Readme?

Love the library btw! ๐Ÿ‘

SimpleParsing has unlisted numpy dependency since 0.0.14

Describe the bug
SimpleParsing 0.0.14 added (among others) the module simple_parsing.helpers.hdparams.hdparam which imports numpy. Unfortunately, numpy is not listed as a dependency in setup.py, which causes all code that uses SimpleParsing to fail with an ModuleNotFoundError.

Suggested Fix
Either remove the dependency to numpy or explicitly state it as a dependency. Currently, matplotlib is a test dependency, which will install numpy if one installs SimpleParsing with those test dependencies.

Personal opinion: Don't use numpy as a dependency at all. SimpleParsing is quite lightweight and numpy is a very heavy library. Or at least implement the helper module in a way that it can have numpy as an optional dependency, such that one is not forced to install it.

Lists of enums are parsed by value on the command line

Describe the bug
While enums seem to be parsed by member name, a list of them seems to be parsed by member value, but only if the value is a string, not an integer

To Reproduce

import enum
from typing import *
from dataclasses import dataclass

from simple_parsing import ArgumentParser
from simple_parsing.helpers import list_field

parser = ArgumentParser()

class Color(enum.Enum):
    RED = "RED"
    ORANGE = "ORANGE"
    BLUE = "BLUE"

class Temperature(enum.Enum):
    HOT = 1
    WARM = 0
    COLD = -1
    MONTREAL = -35

@dataclass
class MyPreferences:
    """You can use Enums"""
    color: Color = Color.BLUE # my favorite colour
    # a list of colors
    color_list: List[Color] = list_field(Color.ORANGE)
    # pick a temperature
    temp: Temperature = Temperature.WARM
    # a list of temperatures
    temp_list: List[Temperature] = list_field(Temperature.COLD, Temperature.WARM)

parser.add_arguments(MyPreferences, "my_preferences")
args = parser.parse_args()
prefs: MyPreferences = args.my_preferences
print(prefs)

Expected behavior
A clear and concise description of what you expected to happen.

$ python issue.py --color ORANGE --color_list RED BLUE --temp MONTREAL
MyPreferences(color=<Color.ORANGE: 'ORANGE'>, color_list=[<Color.RED: 'RED'>, <Color.BLUE: 'BLUE'>], temp=<Temperature.MONTREAL: -35>, temp_list=[<Temperature.COLD: -1>, <Temperature.WARM: 0>])
$ python issue.py --color ORANGE --color_list RED BLUE --temp MONTREAL --temp_list MONTREAL
MyPreferences(color=<Color.ORANGE: 'ORANGE'>, color_list=[<Color.RED: 'RED'>, <Color.BLUE: 'BLUE'>], temp=<Temperature.MONTREAL: -35>, temp_list=[<Temperature.MONTREAL: -35>])

Actual behavior
A clear and concise description of what is happening.

$ python issue.py --color ORANGE --color_list RED BLUE --temp MONTREAL --temp_list MONTREAL
usage: enums.py [-h] [--color Color] [--color_list Color] [--temp Temperature]
                [--temp_list Temperature]
enums.py: error: argument --temp_list: invalid Temperature value: 'MONTREAL'

Desktop (please complete the following information):

  • Version 0.0.15.post1
  • Python version: 3.9.0

Additional context
If I add the proper encoders and decoders, I can load and save both kinds of enum lists from .json files just fine:

@encode.register
def encode_Color(obj: Color) -> str:
    return obj.name

register_decoding_fn(Color, Color)

@encode.register
def encode_Temperature(obj: Temperature) -> str:
    return obj.name

register_decoding_fn(Temperature, lambda temp: Temperature[temp])

Support for parallel commands

Is your feature request related to a problem? Please describe.
Argparse has no native support for multiple parallel commands. This would be a great addition to this dataclass based parser.

Describe the solution you'd like
An example is an image processing tool. It would be great to be able to do this:

img.py --input ~/image.jpg resize --scale 0.5 sharpen --radius 0.2 save --quality 85 --file ~/image_edited.jpg

Describe alternatives you've considered
Declarative Parser supports this, but I find your dataclass based parser and strong typing appealing.

Additional context
Click also supports this, but I'd prefer something that integrates with argparse.

Make `Serializable` into a wrapping class

Instead of having to inherit from Serializable, it would be great to just be able to use it as static methods on the dataclasses. The problem now is that it pollutes the namespace of the dataclass so instead of seeing just the fields you are interested in you also see things such as save etc.

Thanks for a much needed and easy to use library!

Option for using only dash variants

There is an option add_option_string_dash_variants:

from dataclasses import dataclass

from simple_parsing import ArgumentParser


@dataclass
class Args:
    enable_this_option: bool  # This is a nice option
    num_spiders_per_web: int = 5  # How many?


parser = ArgumentParser(add_option_string_dash_variants=True)
parser.add_arguments(Args, dest="args")

args = parser.parse_args(["--help"])

However, this always creates two versions of the same option and clutters the help:

  --enable-this-option bool, --enable_this_option bool
                        This is a nice option (default: None)
  --num-spiders-per-web int, --num_spiders_per_web int
                        How many? (default: 5)

It would be nice to have the option to disable the underscore variants and only use the dash variants (for people preferring dashes):

  --enable-this-option bool
                        This is a nice option (default: None)
  --num-spiders-per-web int
                        How many? (default: 5)

Elegantly Supporting Multiple Names for Same Argument, (e.g. "-v", "--verbose")

Is your feature request related to a problem? Please describe.
Often programs use a shorthand for most commands, like "-f --force", "-v --verbose" "-d --delete", etc. It might be worth it to make this mechanism easy to implement, and also neat.

I'm not sure how to implement this, and I'd like to get some external input / feedback

Describe the solution you'd like
A way to define alternative command names that is clear, concise and intuitive.

Describe alternatives you've considered

  • Having some field-like function that takes alternative names and puts it into the field metadata dict
  • Using something from the comments
  • Using a second attribute and linking the two somehow so they always represent the same value

Could you make a release?

The last release was made in August 10.
Since then, some changes have been merged.
I would love to see a new release in the near future.

Set subparser default value

I would like to use subparser with a default value:

I tried:

@dataclasses.dataclass
class Program:
    command: Union[Command0, Command1] = DefaultCommand()

    def execute(self) -> None:
        return self.command.execute()


parser = simple_parsing.ArgumentParser()
parser.add_arguments(Program, dest="program")
parser.parse_args()

And

parser = simple_parsing.ArgumentParser()
parser.add_arguments(Program, dest="program")
parser.set_defaults(**{"program.command": lambda _: parser.print_help()})
parser.parse_args()

But none of them seems to work. Running python my_program.py currently raise:

error: the following arguments are required: program.command

How can this be achieved ?
This can be helpful, for example to display parser.print_help() if no command is provided.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.