diff options
author | CoprDistGit <infra@openeuler.org> | 2023-05-29 11:57:17 +0000 |
---|---|---|
committer | CoprDistGit <infra@openeuler.org> | 2023-05-29 11:57:17 +0000 |
commit | 30dd194fec83c6fc3144d65b679d7f1af18be8df (patch) | |
tree | e318f97374931d2287084440a42bbf40c3a16beb | |
parent | 7522446d6fc1b1ae2ba6c47a37a08c41a78a2a70 (diff) |
automatic import of python-datargs
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | python-datargs.spec | 1311 | ||||
-rw-r--r-- | sources | 1 |
3 files changed, 1313 insertions, 0 deletions
@@ -0,0 +1 @@ +/datargs-0.11.0.tar.gz diff --git a/python-datargs.spec b/python-datargs.spec new file mode 100644 index 0000000..795ac50 --- /dev/null +++ b/python-datargs.spec @@ -0,0 +1,1311 @@ +%global _empty_manifest_terminate_build 0 +Name: python-datargs +Version: 0.11.0 +Release: 1 +Summary: Declarative, type-safe command line argument parsers from dataclasses and attrs classes +License: MIT +URL: https://github.com/roee30/datargs +Source0: https://mirrors.nju.edu.cn/pypi/web/packages/20/95/e11089c21fbb01fae8b6f29ff132a0a4410e5bdd66bda0100ce263f835ec/datargs-0.11.0.tar.gz +BuildArch: noarch + +Requires: python3-attrs +Requires: python3-boltons +Requires: python3-typing-extensions + +%description +# datargs + +A paper-thin wrapper around `argparse` that creates type-safe parsers +from `dataclass` and `attrs` classes. + +## Quickstart + + +Install `datargs`: + +```bash +pip install datargs +``` + +Create a `dataclass` (or an `attrs` class) describing your command line interface, and call +`datargs.parse()` with the class: + +```python +# script.py +from dataclasses import dataclass +from pathlib import Path +from datargs import parse + +@dataclass # or @attr.s(auto_attribs=True) +class Args: + url: str + output_path: Path + verbose: bool + retries: int = 3 + +def main(): + args = parse(Args) + print(args) + +if __name__ == "__main__": + main() +``` + +***(experimental)*** Alternatively: convert an existing parser to a dataclass: +```python +# script.py +parser = ArgumentParser() +parser.add_argument(...) +from datargs import convert +convert(parser) +``` + +`convert()` prints a class definition to the console. +Copy it to your script. + +Mypy and pycharm correctly infer the type of `args` as `Args`, and your script is good to go! +```bash +$ python script.py -h +usage: test.py [-h] --url URL --output-path OUTPUT_PATH [--retries RETRIES] + [--verbose] + +optional arguments: + -h, --help show this help message and exit + --url URL + --output-path OUTPUT_PATH + --retries RETRIES + --verbose +$ python script.py --url "https://..." --output-path out --retries 4 --verbose +Args(url="https://...", output_path=Path("out"), retries=4, verbose=True) +``` + +## Table of Contents + +<!-- toc --> + +- [Features](#features) + * [Static verification](#static-verification) + * [`dataclass`/`attr.s` agnostic](#dataclassattrs-agnostic) + * [Aliases](#aliases) + * [`ArgumentParser` options](#argumentparser-options) + * [Enums](#enums) + * [Sequences, Optionals, and Literals](#sequences-optionals-and-literals) + * [Sub Commands](#sub-commands) +- ["Why not"s and design choices](#why-nots-and-design-choices) + * [Just use argparse?](#just-use-argparse) + * [Use `click`](#use-clickhttpsclickpalletsprojectscomen7x)? + * [Use `clout`](#use-clouthttpscloutreadthedocsioenlatestindexhtml)? + * [Use `simple-parsing`](#use-simple-parsinghttpspypiorgprojectsimple-parsing)? + * [Use `argparse-dataclass`](#use-argparse-dataclasshttpspypiorgprojectargparse-dataclass)? + * [Use `argparse-dataclasses`](#use-argparse-dataclasseshttpspypiorgprojectargparse-dataclasses)? +- [FAQs](#faqs) + * [Is this cross-platform?](#is-this-cross-platform) + * [Why are mutually exclusive options not supported?](#why-are-mutually-exclusive-options-not-supported) + +<!-- tocstop --> + +## Features + +### Static verification +Mypy/Pycharm have your back when you when you make a mistake: +```python +... +def main(): + args = parse(Args) + args.urll # typo +... +``` +Pycharm says: `Unresolved attribute reference 'urll' for class 'Args'`. + +Mypy says: `script.py:15: error: "Args" has no attribute "urll"; maybe "url"?` + + +### `dataclass`/`attr.s` agnostic +```pycon +>>> import attr, datargs +>>> @attr.s +... class Args: +... flag: bool = attr.ib() +>>> datargs.parse(Args, []) +Args(flag=False) +``` + +### Aliases +Aliases and `ArgumentParser.add_argument()` parameters are taken from `metadata`: + +```pycon +>>> from dataclasses import dataclass, field +>>> from datargs import parse +>>> @dataclass +... class Args: +... retries: int = field(default=3, metadata=dict(help="number of retries", aliases=["-r"], metavar="RETRIES")) +>>> parse(Args, ["-h"]) +usage: ... +optional arguments: + -h, --help show this help message and exit + --retries RETRIES, -r RETRIES +>>> parse(Args, ["-r", "4"]) +Args(retries=4) +``` + +`arg` is a replacement for `field` that puts `add_argument()` parameters in `metadata`. +Use it to save precious keystrokes: +```pycon +>>> 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"]) +# exactly the same as before +``` + +**NOTE**: `arg()` does not currently work with `attr.s`. + +`arg()` also supports all `field`/`attr.ib()` keyword arguments. + + +### `ArgumentParser` options +You can pass `ArgumnetParser` keyword arguments to `argsclass`. +Description is its own parameter - the rest are passed as the `parser_params` parameter as a `dict`. + +When a class is used as a subcommand (see below), `parser_params` are passed to `add_parser`, including `aliases`. +```pycon +>>> from datargs import parse, argsclass +>>> @argsclass(description="Romans go home!", parser_params=dict(prog="messiah.py")) +... class Args: +... flag: bool +>>> parse(Args, ["-h"], parser=parser) +usage: messiah.py [-h] [--flag] +Romans go home! +... +``` + +or you can pass your own parser: +```pycon +>>> from argparse import ArgumentParser +>>> from datargs import parse, argsclass +>>> @argsclass +... class Args: +... flag: bool +>>> parser = ArgumentParser(description="Romans go home!", prog="messiah.py") +>>> parse(Args, ["-h"], parser=parser) +usage: messiah.py [-h] [--flag] +Romans go home! +... +``` + +Use `make_parser()` to create a parser and save it for later: +```pycon +>>> from datargs import make_parser +>>> @dataclass +... class Args: +... ... +>>> parser = make_parser(Args) # pass `parser=...` to modify an existing parser +``` +**NOTE**: passing your own parser ignores `ArgumentParser` params passed to `argsclass()`. + +### Enums +With `datargs`, enums Just Work™: + +```pycon +>>> import enum, attr, datargs +>>> class FoodEnum(enum.Enum): +... ham = 0 +... spam = 1 +>>> @attr.dataclass +... class Args: +... food: FoodEnum +>>> datargs.parse(Args, ["--food", "ham"]) +Args(food=<FoodEnum.ham: 0>) +>>> datargs.parse(Args, ["--food", "eggs"]) +usage: enum_test.py [-h] --food {ham,spam} +enum_test.py: error: argument --food: invalid choice: 'eggs' (choose from ['ham', 'spam']) +``` + +**NOTE**: enums are passed by name on the command line and not by value. + +## Sequences, Optionals, and Literals +Have a `Sequence` or a `List` of something to +automatically use `nargs`: + + +```python +from pathlib import Path +from dataclasses import dataclass +from typing import Sequence +from datargs import parse + +@dataclass +class Args: + # same as nargs='*' + files: Sequence[Path] = () + +args = parse(Args, ["--files", "foo.txt", "bar.txt"]) +assert args.files == [Path("foo.txt"), Path("bar.txt")] +``` + +Specify a list of positional parameters like so: + +```python +from datargs import argsclass, arg +@argsclass +class Args: + arg: Sequence[int] = arg(default=(), positional=True) +``` + +`Optional` arguments default to `None`: + +```python +from pathlib import Path +from dataclasses import dataclass +from typing import Optional +from datargs import parse + +@dataclass +class Args: + path: Optional[Path] = None + +args = parse(Args, ["--path", "foo.txt"]) +assert args.path == Path("foo.txt") + +args = parse(Args, []) +assert args.path is None +``` + +And `Literal` can be used to specify choices: + +```python +from pathlib import Path +from dataclasses import dataclass +from typing import Literal +from datargs import parse + +@dataclass +class Args: + path: Literal[Path("foo.txt"), Path("bar.txt")] + +args = parse(Args, ["--path", "foo.txt"]) +assert args.path == Path("foo.txt") + +# Throws an error! +args = parse(Args, ["--path", "bad-option.txt"]) +``` + +### Sub Commands + +No need to specify a useless `dest` to dispatch on different commands. +A `Union` of dataclasses/attrs classes automatically becomes a group of subparsers. +The attribute holding the `Union` holds the appropriate instance +upon parsing, making your code type-safe: + +```python +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: typing.Union[Install, Show] + log: str = None + +args = parse(Pip, ["--log", "debug.log", "install", "my_package"]) +print(args) +# prints: Pip(action=Install(package='my_package'), log='debug.log') + +# Consume arguments: +if args.log: + logging.basicConfig(filename=args.log) +if isinstance(args.action, Install): + install_package(args.action.package) + # static type error: args.action.verbose +elif isinstance(args.action, Show): + list_all_packages(verbose=args.action.verbose) +else: + assert False, "Unreachable code" +``` +Command name is derived from class name. To change this, use the `name` parameter to `@argsclass`. + +As with all other parameters to `add_parser`, +`aliases` can be passed as a key in `parser_params` to add subcommand aliases. + +**NOTE**: if the commented-out line above does not issue a type error, try adding an `@dataclass/@attr.s` +before or instead of `@argsclass()`: + +```python +@argsclass(description="Pip Install Packages!") # optional +@dataclass +class Pip: + action: typing.Union[Install, Show] + log: str = None +... +if isinstance(args.action, Install): + install_package(args.action.package) + # this should now produce a type error: args.action.verbose +``` + +## "Why not"s and design choices +Many libraries out there do similar things. This list serves as documentation for existing solutions and differences. + +So, why not... + +### Just use argparse? +That's easy. The interface is clumsy and repetitive, a.k.a boilerplate. Additionally, `ArgumentParser.parse_args()` returns a `Namespace`, which is +equivalent to `Any`, meaning that it any attribute access is legal when type checking. Alas, invalid attribute access will fail at runtime. For example: +```python +def parse_args(): + parser = ArgumentParser() + parser.add_argument("--url") + return parser.parse_args() + +def main(): + args = parse_args() + print(args.url) +``` + +Let's say for some reason `--url` is changed to `--uri`: + +```python +parser.add_argument("--uri") +... +print(args.url) # oops +``` +You won't discover you made a mistake until you run the code. With `datargs`, a static type checker will issue an error. +Also, why use a carriage when you have a spaceship? + +### Use [`click`](https://click.palletsprojects.com/en/7.x/)? +`click` is a great library. It provides many utilities for command line programs. + +Use `datargs` if you believe user interface should not be coupled with implementation, or if you +want to use `argparse` without boilerplate. +Use `click` if you don't care. + + +### Use [`clout`](https://clout.readthedocs.io/en/latest/index.html)? +It seems that `clout` aims to be an end-to-end solution for command line programs à la click. + +Use it if you need a broader solution. Use `datargs` if you want to use `argparse` without boilerplate. + +### Use [`simple-parsing`](https://pypi.org/project/simple-parsing/)? +This is another impressive library. + +Use it if you have deeply-nested options, or if the following points don't apply +to you. + +Use `datargs` if you: +* need `attrs` support +* want as little magic as possible +* don't have many options or they're not nested +* prefer dashes (`--like-this`) over underscores (`--like_this`) + +### Use [`argparse-dataclass`](https://pypi.org/project/argparse-dataclass/)? +It's similar to this library. The main differences I found are: +* no `attrs` support +* not on github, so who you gonna call? + +### Use [`argparse-dataclasses`](https://pypi.org/project/argparse-dataclasses/)? +Same points `argparse-dataclass` but also [Uses inheritance](https://refactoring.guru/replace-inheritance-with-delegation). + +## FAQs +### Is this cross-platform? +Yes, just like `argparse`. +If you find a bug on a certain platform (or any other bug), please report it. + +### Why are mutually exclusive options not supported? + +This library is based on the idea of a one-to-one correspondence between most parsers +and simple classes. Conceptually, mutually exclusive options are analogous to +[sum types](https://en.wikipedia.org/wiki/Tagged_union), just like [subparsers](#sub-commands) are, +but writing a class for each flag is not ergonomic enough. +Contact me if you want this feature or if you come up with a better solution. + + +%package -n python3-datargs +Summary: Declarative, type-safe command line argument parsers from dataclasses and attrs classes +Provides: python-datargs +BuildRequires: python3-devel +BuildRequires: python3-setuptools +BuildRequires: python3-pip +%description -n python3-datargs +# datargs + +A paper-thin wrapper around `argparse` that creates type-safe parsers +from `dataclass` and `attrs` classes. + +## Quickstart + + +Install `datargs`: + +```bash +pip install datargs +``` + +Create a `dataclass` (or an `attrs` class) describing your command line interface, and call +`datargs.parse()` with the class: + +```python +# script.py +from dataclasses import dataclass +from pathlib import Path +from datargs import parse + +@dataclass # or @attr.s(auto_attribs=True) +class Args: + url: str + output_path: Path + verbose: bool + retries: int = 3 + +def main(): + args = parse(Args) + print(args) + +if __name__ == "__main__": + main() +``` + +***(experimental)*** Alternatively: convert an existing parser to a dataclass: +```python +# script.py +parser = ArgumentParser() +parser.add_argument(...) +from datargs import convert +convert(parser) +``` + +`convert()` prints a class definition to the console. +Copy it to your script. + +Mypy and pycharm correctly infer the type of `args` as `Args`, and your script is good to go! +```bash +$ python script.py -h +usage: test.py [-h] --url URL --output-path OUTPUT_PATH [--retries RETRIES] + [--verbose] + +optional arguments: + -h, --help show this help message and exit + --url URL + --output-path OUTPUT_PATH + --retries RETRIES + --verbose +$ python script.py --url "https://..." --output-path out --retries 4 --verbose +Args(url="https://...", output_path=Path("out"), retries=4, verbose=True) +``` + +## Table of Contents + +<!-- toc --> + +- [Features](#features) + * [Static verification](#static-verification) + * [`dataclass`/`attr.s` agnostic](#dataclassattrs-agnostic) + * [Aliases](#aliases) + * [`ArgumentParser` options](#argumentparser-options) + * [Enums](#enums) + * [Sequences, Optionals, and Literals](#sequences-optionals-and-literals) + * [Sub Commands](#sub-commands) +- ["Why not"s and design choices](#why-nots-and-design-choices) + * [Just use argparse?](#just-use-argparse) + * [Use `click`](#use-clickhttpsclickpalletsprojectscomen7x)? + * [Use `clout`](#use-clouthttpscloutreadthedocsioenlatestindexhtml)? + * [Use `simple-parsing`](#use-simple-parsinghttpspypiorgprojectsimple-parsing)? + * [Use `argparse-dataclass`](#use-argparse-dataclasshttpspypiorgprojectargparse-dataclass)? + * [Use `argparse-dataclasses`](#use-argparse-dataclasseshttpspypiorgprojectargparse-dataclasses)? +- [FAQs](#faqs) + * [Is this cross-platform?](#is-this-cross-platform) + * [Why are mutually exclusive options not supported?](#why-are-mutually-exclusive-options-not-supported) + +<!-- tocstop --> + +## Features + +### Static verification +Mypy/Pycharm have your back when you when you make a mistake: +```python +... +def main(): + args = parse(Args) + args.urll # typo +... +``` +Pycharm says: `Unresolved attribute reference 'urll' for class 'Args'`. + +Mypy says: `script.py:15: error: "Args" has no attribute "urll"; maybe "url"?` + + +### `dataclass`/`attr.s` agnostic +```pycon +>>> import attr, datargs +>>> @attr.s +... class Args: +... flag: bool = attr.ib() +>>> datargs.parse(Args, []) +Args(flag=False) +``` + +### Aliases +Aliases and `ArgumentParser.add_argument()` parameters are taken from `metadata`: + +```pycon +>>> from dataclasses import dataclass, field +>>> from datargs import parse +>>> @dataclass +... class Args: +... retries: int = field(default=3, metadata=dict(help="number of retries", aliases=["-r"], metavar="RETRIES")) +>>> parse(Args, ["-h"]) +usage: ... +optional arguments: + -h, --help show this help message and exit + --retries RETRIES, -r RETRIES +>>> parse(Args, ["-r", "4"]) +Args(retries=4) +``` + +`arg` is a replacement for `field` that puts `add_argument()` parameters in `metadata`. +Use it to save precious keystrokes: +```pycon +>>> 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"]) +# exactly the same as before +``` + +**NOTE**: `arg()` does not currently work with `attr.s`. + +`arg()` also supports all `field`/`attr.ib()` keyword arguments. + + +### `ArgumentParser` options +You can pass `ArgumnetParser` keyword arguments to `argsclass`. +Description is its own parameter - the rest are passed as the `parser_params` parameter as a `dict`. + +When a class is used as a subcommand (see below), `parser_params` are passed to `add_parser`, including `aliases`. +```pycon +>>> from datargs import parse, argsclass +>>> @argsclass(description="Romans go home!", parser_params=dict(prog="messiah.py")) +... class Args: +... flag: bool +>>> parse(Args, ["-h"], parser=parser) +usage: messiah.py [-h] [--flag] +Romans go home! +... +``` + +or you can pass your own parser: +```pycon +>>> from argparse import ArgumentParser +>>> from datargs import parse, argsclass +>>> @argsclass +... class Args: +... flag: bool +>>> parser = ArgumentParser(description="Romans go home!", prog="messiah.py") +>>> parse(Args, ["-h"], parser=parser) +usage: messiah.py [-h] [--flag] +Romans go home! +... +``` + +Use `make_parser()` to create a parser and save it for later: +```pycon +>>> from datargs import make_parser +>>> @dataclass +... class Args: +... ... +>>> parser = make_parser(Args) # pass `parser=...` to modify an existing parser +``` +**NOTE**: passing your own parser ignores `ArgumentParser` params passed to `argsclass()`. + +### Enums +With `datargs`, enums Just Work™: + +```pycon +>>> import enum, attr, datargs +>>> class FoodEnum(enum.Enum): +... ham = 0 +... spam = 1 +>>> @attr.dataclass +... class Args: +... food: FoodEnum +>>> datargs.parse(Args, ["--food", "ham"]) +Args(food=<FoodEnum.ham: 0>) +>>> datargs.parse(Args, ["--food", "eggs"]) +usage: enum_test.py [-h] --food {ham,spam} +enum_test.py: error: argument --food: invalid choice: 'eggs' (choose from ['ham', 'spam']) +``` + +**NOTE**: enums are passed by name on the command line and not by value. + +## Sequences, Optionals, and Literals +Have a `Sequence` or a `List` of something to +automatically use `nargs`: + + +```python +from pathlib import Path +from dataclasses import dataclass +from typing import Sequence +from datargs import parse + +@dataclass +class Args: + # same as nargs='*' + files: Sequence[Path] = () + +args = parse(Args, ["--files", "foo.txt", "bar.txt"]) +assert args.files == [Path("foo.txt"), Path("bar.txt")] +``` + +Specify a list of positional parameters like so: + +```python +from datargs import argsclass, arg +@argsclass +class Args: + arg: Sequence[int] = arg(default=(), positional=True) +``` + +`Optional` arguments default to `None`: + +```python +from pathlib import Path +from dataclasses import dataclass +from typing import Optional +from datargs import parse + +@dataclass +class Args: + path: Optional[Path] = None + +args = parse(Args, ["--path", "foo.txt"]) +assert args.path == Path("foo.txt") + +args = parse(Args, []) +assert args.path is None +``` + +And `Literal` can be used to specify choices: + +```python +from pathlib import Path +from dataclasses import dataclass +from typing import Literal +from datargs import parse + +@dataclass +class Args: + path: Literal[Path("foo.txt"), Path("bar.txt")] + +args = parse(Args, ["--path", "foo.txt"]) +assert args.path == Path("foo.txt") + +# Throws an error! +args = parse(Args, ["--path", "bad-option.txt"]) +``` + +### Sub Commands + +No need to specify a useless `dest` to dispatch on different commands. +A `Union` of dataclasses/attrs classes automatically becomes a group of subparsers. +The attribute holding the `Union` holds the appropriate instance +upon parsing, making your code type-safe: + +```python +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: typing.Union[Install, Show] + log: str = None + +args = parse(Pip, ["--log", "debug.log", "install", "my_package"]) +print(args) +# prints: Pip(action=Install(package='my_package'), log='debug.log') + +# Consume arguments: +if args.log: + logging.basicConfig(filename=args.log) +if isinstance(args.action, Install): + install_package(args.action.package) + # static type error: args.action.verbose +elif isinstance(args.action, Show): + list_all_packages(verbose=args.action.verbose) +else: + assert False, "Unreachable code" +``` +Command name is derived from class name. To change this, use the `name` parameter to `@argsclass`. + +As with all other parameters to `add_parser`, +`aliases` can be passed as a key in `parser_params` to add subcommand aliases. + +**NOTE**: if the commented-out line above does not issue a type error, try adding an `@dataclass/@attr.s` +before or instead of `@argsclass()`: + +```python +@argsclass(description="Pip Install Packages!") # optional +@dataclass +class Pip: + action: typing.Union[Install, Show] + log: str = None +... +if isinstance(args.action, Install): + install_package(args.action.package) + # this should now produce a type error: args.action.verbose +``` + +## "Why not"s and design choices +Many libraries out there do similar things. This list serves as documentation for existing solutions and differences. + +So, why not... + +### Just use argparse? +That's easy. The interface is clumsy and repetitive, a.k.a boilerplate. Additionally, `ArgumentParser.parse_args()` returns a `Namespace`, which is +equivalent to `Any`, meaning that it any attribute access is legal when type checking. Alas, invalid attribute access will fail at runtime. For example: +```python +def parse_args(): + parser = ArgumentParser() + parser.add_argument("--url") + return parser.parse_args() + +def main(): + args = parse_args() + print(args.url) +``` + +Let's say for some reason `--url` is changed to `--uri`: + +```python +parser.add_argument("--uri") +... +print(args.url) # oops +``` +You won't discover you made a mistake until you run the code. With `datargs`, a static type checker will issue an error. +Also, why use a carriage when you have a spaceship? + +### Use [`click`](https://click.palletsprojects.com/en/7.x/)? +`click` is a great library. It provides many utilities for command line programs. + +Use `datargs` if you believe user interface should not be coupled with implementation, or if you +want to use `argparse` without boilerplate. +Use `click` if you don't care. + + +### Use [`clout`](https://clout.readthedocs.io/en/latest/index.html)? +It seems that `clout` aims to be an end-to-end solution for command line programs à la click. + +Use it if you need a broader solution. Use `datargs` if you want to use `argparse` without boilerplate. + +### Use [`simple-parsing`](https://pypi.org/project/simple-parsing/)? +This is another impressive library. + +Use it if you have deeply-nested options, or if the following points don't apply +to you. + +Use `datargs` if you: +* need `attrs` support +* want as little magic as possible +* don't have many options or they're not nested +* prefer dashes (`--like-this`) over underscores (`--like_this`) + +### Use [`argparse-dataclass`](https://pypi.org/project/argparse-dataclass/)? +It's similar to this library. The main differences I found are: +* no `attrs` support +* not on github, so who you gonna call? + +### Use [`argparse-dataclasses`](https://pypi.org/project/argparse-dataclasses/)? +Same points `argparse-dataclass` but also [Uses inheritance](https://refactoring.guru/replace-inheritance-with-delegation). + +## FAQs +### Is this cross-platform? +Yes, just like `argparse`. +If you find a bug on a certain platform (or any other bug), please report it. + +### Why are mutually exclusive options not supported? + +This library is based on the idea of a one-to-one correspondence between most parsers +and simple classes. Conceptually, mutually exclusive options are analogous to +[sum types](https://en.wikipedia.org/wiki/Tagged_union), just like [subparsers](#sub-commands) are, +but writing a class for each flag is not ergonomic enough. +Contact me if you want this feature or if you come up with a better solution. + + +%package help +Summary: Development documents and examples for datargs +Provides: python3-datargs-doc +%description help +# datargs + +A paper-thin wrapper around `argparse` that creates type-safe parsers +from `dataclass` and `attrs` classes. + +## Quickstart + + +Install `datargs`: + +```bash +pip install datargs +``` + +Create a `dataclass` (or an `attrs` class) describing your command line interface, and call +`datargs.parse()` with the class: + +```python +# script.py +from dataclasses import dataclass +from pathlib import Path +from datargs import parse + +@dataclass # or @attr.s(auto_attribs=True) +class Args: + url: str + output_path: Path + verbose: bool + retries: int = 3 + +def main(): + args = parse(Args) + print(args) + +if __name__ == "__main__": + main() +``` + +***(experimental)*** Alternatively: convert an existing parser to a dataclass: +```python +# script.py +parser = ArgumentParser() +parser.add_argument(...) +from datargs import convert +convert(parser) +``` + +`convert()` prints a class definition to the console. +Copy it to your script. + +Mypy and pycharm correctly infer the type of `args` as `Args`, and your script is good to go! +```bash +$ python script.py -h +usage: test.py [-h] --url URL --output-path OUTPUT_PATH [--retries RETRIES] + [--verbose] + +optional arguments: + -h, --help show this help message and exit + --url URL + --output-path OUTPUT_PATH + --retries RETRIES + --verbose +$ python script.py --url "https://..." --output-path out --retries 4 --verbose +Args(url="https://...", output_path=Path("out"), retries=4, verbose=True) +``` + +## Table of Contents + +<!-- toc --> + +- [Features](#features) + * [Static verification](#static-verification) + * [`dataclass`/`attr.s` agnostic](#dataclassattrs-agnostic) + * [Aliases](#aliases) + * [`ArgumentParser` options](#argumentparser-options) + * [Enums](#enums) + * [Sequences, Optionals, and Literals](#sequences-optionals-and-literals) + * [Sub Commands](#sub-commands) +- ["Why not"s and design choices](#why-nots-and-design-choices) + * [Just use argparse?](#just-use-argparse) + * [Use `click`](#use-clickhttpsclickpalletsprojectscomen7x)? + * [Use `clout`](#use-clouthttpscloutreadthedocsioenlatestindexhtml)? + * [Use `simple-parsing`](#use-simple-parsinghttpspypiorgprojectsimple-parsing)? + * [Use `argparse-dataclass`](#use-argparse-dataclasshttpspypiorgprojectargparse-dataclass)? + * [Use `argparse-dataclasses`](#use-argparse-dataclasseshttpspypiorgprojectargparse-dataclasses)? +- [FAQs](#faqs) + * [Is this cross-platform?](#is-this-cross-platform) + * [Why are mutually exclusive options not supported?](#why-are-mutually-exclusive-options-not-supported) + +<!-- tocstop --> + +## Features + +### Static verification +Mypy/Pycharm have your back when you when you make a mistake: +```python +... +def main(): + args = parse(Args) + args.urll # typo +... +``` +Pycharm says: `Unresolved attribute reference 'urll' for class 'Args'`. + +Mypy says: `script.py:15: error: "Args" has no attribute "urll"; maybe "url"?` + + +### `dataclass`/`attr.s` agnostic +```pycon +>>> import attr, datargs +>>> @attr.s +... class Args: +... flag: bool = attr.ib() +>>> datargs.parse(Args, []) +Args(flag=False) +``` + +### Aliases +Aliases and `ArgumentParser.add_argument()` parameters are taken from `metadata`: + +```pycon +>>> from dataclasses import dataclass, field +>>> from datargs import parse +>>> @dataclass +... class Args: +... retries: int = field(default=3, metadata=dict(help="number of retries", aliases=["-r"], metavar="RETRIES")) +>>> parse(Args, ["-h"]) +usage: ... +optional arguments: + -h, --help show this help message and exit + --retries RETRIES, -r RETRIES +>>> parse(Args, ["-r", "4"]) +Args(retries=4) +``` + +`arg` is a replacement for `field` that puts `add_argument()` parameters in `metadata`. +Use it to save precious keystrokes: +```pycon +>>> 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"]) +# exactly the same as before +``` + +**NOTE**: `arg()` does not currently work with `attr.s`. + +`arg()` also supports all `field`/`attr.ib()` keyword arguments. + + +### `ArgumentParser` options +You can pass `ArgumnetParser` keyword arguments to `argsclass`. +Description is its own parameter - the rest are passed as the `parser_params` parameter as a `dict`. + +When a class is used as a subcommand (see below), `parser_params` are passed to `add_parser`, including `aliases`. +```pycon +>>> from datargs import parse, argsclass +>>> @argsclass(description="Romans go home!", parser_params=dict(prog="messiah.py")) +... class Args: +... flag: bool +>>> parse(Args, ["-h"], parser=parser) +usage: messiah.py [-h] [--flag] +Romans go home! +... +``` + +or you can pass your own parser: +```pycon +>>> from argparse import ArgumentParser +>>> from datargs import parse, argsclass +>>> @argsclass +... class Args: +... flag: bool +>>> parser = ArgumentParser(description="Romans go home!", prog="messiah.py") +>>> parse(Args, ["-h"], parser=parser) +usage: messiah.py [-h] [--flag] +Romans go home! +... +``` + +Use `make_parser()` to create a parser and save it for later: +```pycon +>>> from datargs import make_parser +>>> @dataclass +... class Args: +... ... +>>> parser = make_parser(Args) # pass `parser=...` to modify an existing parser +``` +**NOTE**: passing your own parser ignores `ArgumentParser` params passed to `argsclass()`. + +### Enums +With `datargs`, enums Just Work™: + +```pycon +>>> import enum, attr, datargs +>>> class FoodEnum(enum.Enum): +... ham = 0 +... spam = 1 +>>> @attr.dataclass +... class Args: +... food: FoodEnum +>>> datargs.parse(Args, ["--food", "ham"]) +Args(food=<FoodEnum.ham: 0>) +>>> datargs.parse(Args, ["--food", "eggs"]) +usage: enum_test.py [-h] --food {ham,spam} +enum_test.py: error: argument --food: invalid choice: 'eggs' (choose from ['ham', 'spam']) +``` + +**NOTE**: enums are passed by name on the command line and not by value. + +## Sequences, Optionals, and Literals +Have a `Sequence` or a `List` of something to +automatically use `nargs`: + + +```python +from pathlib import Path +from dataclasses import dataclass +from typing import Sequence +from datargs import parse + +@dataclass +class Args: + # same as nargs='*' + files: Sequence[Path] = () + +args = parse(Args, ["--files", "foo.txt", "bar.txt"]) +assert args.files == [Path("foo.txt"), Path("bar.txt")] +``` + +Specify a list of positional parameters like so: + +```python +from datargs import argsclass, arg +@argsclass +class Args: + arg: Sequence[int] = arg(default=(), positional=True) +``` + +`Optional` arguments default to `None`: + +```python +from pathlib import Path +from dataclasses import dataclass +from typing import Optional +from datargs import parse + +@dataclass +class Args: + path: Optional[Path] = None + +args = parse(Args, ["--path", "foo.txt"]) +assert args.path == Path("foo.txt") + +args = parse(Args, []) +assert args.path is None +``` + +And `Literal` can be used to specify choices: + +```python +from pathlib import Path +from dataclasses import dataclass +from typing import Literal +from datargs import parse + +@dataclass +class Args: + path: Literal[Path("foo.txt"), Path("bar.txt")] + +args = parse(Args, ["--path", "foo.txt"]) +assert args.path == Path("foo.txt") + +# Throws an error! +args = parse(Args, ["--path", "bad-option.txt"]) +``` + +### Sub Commands + +No need to specify a useless `dest` to dispatch on different commands. +A `Union` of dataclasses/attrs classes automatically becomes a group of subparsers. +The attribute holding the `Union` holds the appropriate instance +upon parsing, making your code type-safe: + +```python +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: typing.Union[Install, Show] + log: str = None + +args = parse(Pip, ["--log", "debug.log", "install", "my_package"]) +print(args) +# prints: Pip(action=Install(package='my_package'), log='debug.log') + +# Consume arguments: +if args.log: + logging.basicConfig(filename=args.log) +if isinstance(args.action, Install): + install_package(args.action.package) + # static type error: args.action.verbose +elif isinstance(args.action, Show): + list_all_packages(verbose=args.action.verbose) +else: + assert False, "Unreachable code" +``` +Command name is derived from class name. To change this, use the `name` parameter to `@argsclass`. + +As with all other parameters to `add_parser`, +`aliases` can be passed as a key in `parser_params` to add subcommand aliases. + +**NOTE**: if the commented-out line above does not issue a type error, try adding an `@dataclass/@attr.s` +before or instead of `@argsclass()`: + +```python +@argsclass(description="Pip Install Packages!") # optional +@dataclass +class Pip: + action: typing.Union[Install, Show] + log: str = None +... +if isinstance(args.action, Install): + install_package(args.action.package) + # this should now produce a type error: args.action.verbose +``` + +## "Why not"s and design choices +Many libraries out there do similar things. This list serves as documentation for existing solutions and differences. + +So, why not... + +### Just use argparse? +That's easy. The interface is clumsy and repetitive, a.k.a boilerplate. Additionally, `ArgumentParser.parse_args()` returns a `Namespace`, which is +equivalent to `Any`, meaning that it any attribute access is legal when type checking. Alas, invalid attribute access will fail at runtime. For example: +```python +def parse_args(): + parser = ArgumentParser() + parser.add_argument("--url") + return parser.parse_args() + +def main(): + args = parse_args() + print(args.url) +``` + +Let's say for some reason `--url` is changed to `--uri`: + +```python +parser.add_argument("--uri") +... +print(args.url) # oops +``` +You won't discover you made a mistake until you run the code. With `datargs`, a static type checker will issue an error. +Also, why use a carriage when you have a spaceship? + +### Use [`click`](https://click.palletsprojects.com/en/7.x/)? +`click` is a great library. It provides many utilities for command line programs. + +Use `datargs` if you believe user interface should not be coupled with implementation, or if you +want to use `argparse` without boilerplate. +Use `click` if you don't care. + + +### Use [`clout`](https://clout.readthedocs.io/en/latest/index.html)? +It seems that `clout` aims to be an end-to-end solution for command line programs à la click. + +Use it if you need a broader solution. Use `datargs` if you want to use `argparse` without boilerplate. + +### Use [`simple-parsing`](https://pypi.org/project/simple-parsing/)? +This is another impressive library. + +Use it if you have deeply-nested options, or if the following points don't apply +to you. + +Use `datargs` if you: +* need `attrs` support +* want as little magic as possible +* don't have many options or they're not nested +* prefer dashes (`--like-this`) over underscores (`--like_this`) + +### Use [`argparse-dataclass`](https://pypi.org/project/argparse-dataclass/)? +It's similar to this library. The main differences I found are: +* no `attrs` support +* not on github, so who you gonna call? + +### Use [`argparse-dataclasses`](https://pypi.org/project/argparse-dataclasses/)? +Same points `argparse-dataclass` but also [Uses inheritance](https://refactoring.guru/replace-inheritance-with-delegation). + +## FAQs +### Is this cross-platform? +Yes, just like `argparse`. +If you find a bug on a certain platform (or any other bug), please report it. + +### Why are mutually exclusive options not supported? + +This library is based on the idea of a one-to-one correspondence between most parsers +and simple classes. Conceptually, mutually exclusive options are analogous to +[sum types](https://en.wikipedia.org/wiki/Tagged_union), just like [subparsers](#sub-commands) are, +but writing a class for each flag is not ergonomic enough. +Contact me if you want this feature or if you come up with a better solution. + + +%prep +%autosetup -n datargs-0.11.0 + +%build +%py3_build + +%install +%py3_install +install -d -m755 %{buildroot}/%{_pkgdocdir} +if [ -d doc ]; then cp -arf doc %{buildroot}/%{_pkgdocdir}; fi +if [ -d docs ]; then cp -arf docs %{buildroot}/%{_pkgdocdir}; fi +if [ -d example ]; then cp -arf example %{buildroot}/%{_pkgdocdir}; fi +if [ -d examples ]; then cp -arf examples %{buildroot}/%{_pkgdocdir}; fi +pushd %{buildroot} +if [ -d usr/lib ]; then + find usr/lib -type f -printf "/%h/%f\n" >> filelist.lst +fi +if [ -d usr/lib64 ]; then + find usr/lib64 -type f -printf "/%h/%f\n" >> filelist.lst +fi +if [ -d usr/bin ]; then + find usr/bin -type f -printf "/%h/%f\n" >> filelist.lst +fi +if [ -d usr/sbin ]; then + find usr/sbin -type f -printf "/%h/%f\n" >> filelist.lst +fi +touch doclist.lst +if [ -d usr/share/man ]; then + find usr/share/man -type f -printf "/%h/%f.gz\n" >> doclist.lst +fi +popd +mv %{buildroot}/filelist.lst . +mv %{buildroot}/doclist.lst . + +%files -n python3-datargs -f filelist.lst +%dir %{python3_sitelib}/* + +%files help -f doclist.lst +%{_docdir}/* + +%changelog +* Mon May 29 2023 Python_Bot <Python_Bot@openeuler.org> - 0.11.0-1 +- Package Spec generated @@ -0,0 +1 @@ +af8009e9b022052b37647ad9b93de291 datargs-0.11.0.tar.gz |