%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 - [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) ## 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=) >>> 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 - [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) ## 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=) >>> 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 - [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) ## 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=) >>> 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 * Wed May 31 2023 Python_Bot - 0.11.0-1 - Package Spec generated