roee30 / datargs Goto Github PK
View Code? Open in Web Editor NEWDeclarative, type-safe command line argument parsers from dataclasses and attrs classes
License: MIT License
Declarative, type-safe command line argument parsers from dataclasses and attrs classes
License: MIT License
I understood that the dest
parameter of the original add_argument
function is not necessary because it is taken from the dataclass field name. But I also understood that the this "field name" is used as the first name_or_flags
of the function add_argument
, right?
(Please, tell me if I'm wrong)
So, could you make this "field name" be overwritten in the command line, when you use the aliases
option?
This would be useful for 2 features:
dest
used inside the script be different from the flags we would like to expose in the command lineToday, we can't reproduce the following:
Example:
from argparse import ArgumentParser
parser = ArgumentParser()
parser.add_argument("-u", "--user-name", dest="user")
parser.add_argument("-p", "--file-path", dest="path")
options = parser.parse_args(["-u", "jon", "-p", "file.txt"])
print(options)
Output:
Namespace(user='jon', path='file.txt')
Helper info:
usage: script.py [-h] [-u USER] [-p PATH]
options:
-h, --help show this help message and exit
-u USER, --user-name USER
-p PATH, --file-path PATH
In summary: inside the script we used user
and path
but the command line flags are -u
,--user-name
and -p
,--file-path
Today, the last helper info shown above can not be reproduced with the following:
from dataclasses import dataclass
from datargs import arg, parse
@dataclass
class Args:
user_name: str = arg(aliases=['-u'])
file_path: str = arg(aliases=['-p'])
options = parse(Args,['-h'])
Because the flags are shown after the field names:
usage: script.py [-h] --user-name USER_NAME --file-path FILE_PATH
options:
-h, --help show this help message and exit
--user-name USER_NAME, -u USER_NAME
--file-path FILE_PATH, -p FILE_PATH
The only way to reproduce something similar would be using u
and p
as field names.
So, if you let the "field name" be overwritten by the aliases in the command line (when aliases are used) these 2 features could be added to the library.
BTW. Thank you for this great library!
I was just looking for something like this.
It will crash in if issubclass(dispatch_type, typ):
...
Since python 3.10, Union[X, Y]
is equivalent to X | Y
And the same is for Optional[X]
is equivalent to X | None
(or Union[X, None]
)
But the following raises an error
import typing, logging
from datargs import argsclass, arg, parse
@argsclass(description="install package")
class Install:
package: str = arg(positional=True, help="package to install")
@argsclass(description="show all packages")
class Show:
verbose: bool = arg(help="show extra info")
@argsclass(description="Pip Install Packages!")
class Pip:
action: Install | Show
log: str = None
args = parse(Pip, ["-h"])
Error:
ValueError: __main__.Install | __main__.Show is not callable
Hi!
@roee30 do you have any suggestions for how to make static and dynamic types consistent when using nargs
or optional parameters with a default value of None
?
In particular, I'd like to write something like:
import datargs
from typing import List, Optional
@dataclasses.dataclass(frozen=True)
class CliArgs:
paths: List[str] = datargs.arg(
positional=True,
nargs="+",
help="List of paths.",
)
number: Optional[int] = datargs.arg(default=None)
args = datargs.parse(CliArgs)
# args.paths is a list, args.number is `None` or an integer
But this doesn't work, and I instead have to write:
import datargs
@dataclasses.dataclass(frozen=True)
class CliArgs:
paths: str = datargs.arg(
positional=True,
nargs="+",
help="List of paths.",
)
number: int = datargs.arg(default=None)
args = datargs.parse(CliArgs)
# Dynamically, args.paths is a list, args.number is `None` or an integer
# But type checkers, IDEs, etc will treat args.path as a string, and args.number as always an integer
Thanks!!
class Args:
exclude: typing.Sequence[str] = arg(default=())
args = parse(Args)
print(args.exclude)
# python test.py --exclude a b
# Output: ["a", "b"]
The default argument only accept tuple, but the return type is list.
Expect tuple.
Comaring to simple-parsing, the README says:
Use datargs if you:
- prefer dashes (--like-this) over underscores (--like_this)
This is not true (anymore), as simple-parse also supports the dash variants (as an option you can set globally):
https://github.com/lebrice/SimpleParsing/blob/master/simple_parsing/parsing.py#L50
Hi, thanks a lot for this library!
I would like to use sub commands, but have the ability to set the command name;
that's because I usually give the name ActionNameConfig
or something like that to the classes holding the configuration/arguments, to better indicate what the class is about.
Is there already the option to give the command a custom name or is it something that needs to be implemented?
thanks again, keep up the good work!
Let's say we have two data classes, which both are required. E.g. model config and data config.
Would it be possible to arg-parse a union of both?
argparse can use formatter_class to show default value, hope this lib can supported
parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
When I run import datargs
I get:
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/mnt/beegfs/work/baumgaertner/gen-bioasq/.venv/lib64/python3.6/site-packages/datargs/__init__.py", line 1, in <module>
from .make import parse, arg, make_parser, argsclass
File "/mnt/beegfs/work/baumgaertner/gen-bioasq/.venv/lib64/python3.6/site-packages/datargs/make.py", line 58, in <module>
from typing import (
ImportError: cannot import name 'get_origin'
The first argument of the original add_argument()
method is the name or flags positional argument that can be a series of flags.
The function args()
could accept the same format.
I think this can be achieved with this simple modification:
def arg(
+ *aliases: str,
positional=False,
nargs=None,
const=None,
@@ -569,7 +570,6 @@ def arg(
choices=None,
help=None,
metavar=None,
- aliases: Sequence[str] = (),
**kwargs,
) -> Any:
"""
What do you think?
As informed in this section of README.md, the groups of mutually exclusive options are not supported.
I would like to discuss this feature and some solutions.
One idea to support that would be with an additional argument mutually_exclusive_groups
in the argsclass
decorator:
GroupName = str # type alias
GroupRequired = bool # type alias
def argsclass(
...
mutually_exclusive_groups: dict[GroupName, GroupRequired] = None
...
):
It may also be added to the arg
function, receiving the name of the group.
Usage:
from datargs import argsclass, arg
@argsclass(mutually_exclusive_groups={"group1": True})
class Args:
foo: str = arg(mutually_exclusive_group="group1")
bar: str = arg(mutually_exclusive_group="group1")
Is that feasible?
Thank you for this useful & compact library! I noticed that the following fails:
from datargs import parse, arg
from dataclasses import dataclass
import attr
@attr.s(auto_attribs=True)
class Settings:
"""Settings"""
mylist: float = arg(nargs=3)
if __name__ == "__main__":
args = parse(Settings)
print(args)
with
$ python3 datargstest.py --mylist 1 2 3
usage: datargstest.py [-h] [--mylist MYLIST]
datargstest.py: error: unrecognized arguments: 2 3
However, if I replace attr.s(auto_attribs=True)
with dataclass
, it works as expected:
$ python3 datargstest.py --mylist 1 2 3
Settings(mylist=[1.0, 2.0, 3.0])
I am having issues using Enum for an optional parameter. It looks like support for optional typing was added in #4, so it may be the Enum class causing the issue.
class Item(enum.Enum):
car = 0
truck = 0
@dataclass
class Args:
item: typing.Optional[Item] = None
Using with optional gives me this error:
error: argument --item: invalid Item value: 'car'
Trying without Optional
and a default of None
works, but the type checker doesn't like it:
Expression of type "Item | None" cannot be assigned to declared type "Item"
I am trying to add a verbosity parameter using action="count"
.
Dataclass example:
@dataclass
class Args:
"""Main program options."""
verbosity: int = field(
default=0,
metadata=dict(
aliases=["-v"],
help="Increase logging verbosity",
action="count",
),
)
I get this error:
Traceback (most recent call last):
File "/home/nick/git/meetingtool/meetingtool.py", line 285, in <module>
main()
File "/home/nick/git/meetingtool/meetingtool.py", line 202, in main
args = parse(Args)
File "/home/nick/git/meetingtool/.venv/lib/python3.9/site-packages/datargs/make.py", line 387, in parse
result = vars(make_parser(cls, parser=parser).parse_args(args))
File "/home/nick/git/meetingtool/.venv/lib/python3.9/site-packages/datargs/make.py", line 294, in make_parser
return _make_parser(record_class, parser=parser)
File "/home/nick/git/meetingtool/.venv/lib/python3.9/site-packages/datargs/make.py", line 335, in _make_parser
parser.add_argument(*action.args, **action.kwargs)
File "/home/nick/.asdf/installs/python/3.9.1/lib/python3.9/argparse.py", line 1416, in add_argument
action = action_class(**kwargs)
TypeError: __init__() got an unexpected keyword argument 'type'
If I comment out action="count"
, I get:
Args(verbosity=0)
If there's some other way of doing it, I haven't found it.
File "/venv/lib/python3.8/site-packages/datargs/make.py", line 224, in sequence_arg
assert nargs in ("+", "?") or isinstance(nargs, int)
AssertionError
With argparse.add_argument
it is possible to convert the argument passed on the command line to a different type. It would be useful to have this functionality using datargs
, but does not seem possible (e.g., type
is not a supported keyword for the arg
utility function).
For example, I have a command line option for setting the log level. I would like the end user to be able to specify the level using its name, a string, but stored in the dataclass
as an int so that it can be directly used to setup the logging configuration.
It's certainly possible to do the conversion afterwards, but it would be more convenient to have the transformation done during parsing. Currently, I might reuse the same arguments dataclass
in multiple programs, which requires me to apply the type transformation in each program, versus having it done transparently by the parser.
[Edit]
One other thing that doing the conversion client side makes less convenient are choices
. Using the type conversion with argparse
it is easy to make string choices case insensitive, but doing it client side requires the command line argument to match the case exactly.
The following snippet returns a error by pylance/pyright (in VSCode) in function arg
from dataclasses import dataclass
from datargs import parse, arg
@dataclass
class Args:
retries: int = arg(default=3, help="number of retries", aliases=["-r"], metavar="RETRIES")
parse(Args, ["-h"])
The message is:
Expression of type "Unknown | _MISSING_TYPE" cannot be assigned to declared type "int"
Type "Unknown | _MISSING_TYPE" cannot be assigned to type "int"
"_MISSING_TYPE" is incompatible with "int"
It appears the parser breaks when using the |
operator in conjunction with importing annotations
from __futures__
. On python 3.12
with datargs
installed from main
this example errors out:
from __future__ import annotations
from datargs import arg, argsclass, parse
@argsclass
class Eat:
food: str = arg(positional=True)
def run(self):
print("eating", self.food)
@argsclass
class Sleep:
time: str = arg(positional=True)
def run(self):
print("sleeping", self.time)
@argsclass
class App:
action: Eat | Sleep
def run(self):
self.action.run()
if __name__ == "__main__":
args = parse(App)
args.run()
Traceback (most recent call last):
...
File ".../lib/python3.12/site-packages/datargs/make.py", line 481, in parse
result = vars(make_parser(cls, parser=parser).parse_args(args))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File ".../lib/python3.12/site-packages/datargs/make.py", line 372, in make_parser
return _make_parser(record_class, parser=parser)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File ".../lib/python3.12/site-packages/datargs/make.py", line 420, in _make_parser
parser.add_argument(*action.args, **action.kwargs)
File ".../lib/python3.12/argparse.py", line 1477, in add_argument
raise ValueError('%r is not callable' % (type_func,))
ValueError: 'Eat | Sleep' is not callable
It works just fine if you don't import from __futures__
.
numbers: Optional[Sequence[int]] = None
crashes in:
if issubclass(typ, rule_typ):
Hi @roee30 !
Would you be able to do a release on PyPI with support for Literal
types?
I just did a code release for some research code that uses datargs, and it'd make dependency tracking a bit easier on my end (cc brentyi/dfgo#1).
A glance at #16 would also be appreciated.
Thanks :)
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.