arcparse


Namearcparse JSON
Version 0.6.12 PyPI version JSON
download
home_pagehttps://github.com/Kuba314/arcparse
SummaryDeclare program arguments declaratively and type-safely
upload_time2024-03-17 10:07:57
maintainer
docs_urlNone
authorJakub Rozek
requires_python>=3.12,<4.0
licenseMIT
keywords argparse declarative argument parsing type-safe
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # arcparse
Declare program arguments type-safely.

This project builds on top of `argparse` by adding type-safety and allowing a more expressive argument parser definition.

Disclaimer: This library is young and relatively unstable. Issues are open and pull requests are welcome!

## Example usage
```py
from arcparse import arcparser, positional

@arcparser
class Args:
    name: str = positional()
    age: int
    happy: bool


args = Args.parse("--age 25 --happy Thomas".split())
print(f"Hi, my name is {args.name}!")
```

For a complete overview of features see [Features](#features).

## Installation
```shell
# Using pip
$ pip install arcparse
```

## Features

### Required and optional arguments
Arguments without explicitly assigned argument class are implicitly options (prefixed with `--`). A non-optional typehint results in `required=True` for options. Defaults can be set by directly assigning them. You can use `option()` to further customize the argument.
```py
@arcparser
class Args:
    required: str
    optional: str | None
    default: str = "foo"
    default_with_help: str = option(default="bar", help="help message")
```

### Positional arguments
Positional arguments use `positional()`. Optional type-hints use `nargs="?"` in the background.
```py
@arcparser
class Args:
    required: str = positional()
    optional: str | None = positional()
```

### Flags
All arguments type-hinted as `bool` are flags, they use `action="store_true"` in the background. Flags (as well as options) can also define short forms for each argument. They can also disable the long form with `short_only=True`.

Use `no_flag()` to easily create a `--no-...` flag with `action="store_false"`.

Use `tri_flag()` (or type-hint argument as `bool | None`) to create a "true" flag and a "false" flag (e.g. `--clone` and `--no-clone`). Passing `--clone` will store `True`, passing `--no-clone` will store `False` and not passing anything will store `None`. Passing both is an error ensured by an implicit mutually exclusive group.
```py
@arcparser
class Args:
    sync: bool
    recurse: bool = no_flag(help="Do not recurse")
    clone: bool | None

    debug: bool = flag("-d")  # both -d and --debug
    verbose: bool = flag("-v", short_only=True)  # only -v
```

### Multiple values per argument
By type-hinting the argument as `list[...]`, the argument will use `nargs="*"` in the background. Passing `at_least_one=True` uses `nargs="+"` instead. Passing `append=True` to `option()` uses `action="append"` instead (this is available only for `option()` and incompatible with `at_least_one`).
```py
@arcparser
class Args:
    option_nargs: list[str]
    positional_nargs: list[str] = positional()
    append_option: list[str] = option(append=True)
    nargs_plus_option: list[str] = option(at_least_one=True)
    nargs_plus_positional: list[str] = positional(at_least_one=True)
```

Note that `option(at_least_one=True)` will cause the option to be required. If this is not intended, provide a default value.

### Name overriding
Passing `name_override=...` will cause the provided string to be used instead of the variable name for the argument name. The string will undergo a replacement of `_` with `-` and will contain a `--` prefix if used in `option()`.

This is useful in combination with accepting multiple values with `append=True`, because the user will use `--value foo --value bar`, while the code will use `args.values`.
```py
@arcparser
class Args:
    values: list[str] = option(name_override="value", append=True)
```

### Type conversions
Automatic type conversions are supported. The type-hint type is used to convert the string argument to the desired type. This is NOT done using argparse's `type=...` because it was causing issues for `dict_option()` and `dict_positional()`. Using a `StrEnum` subclass as a type-hint automatically populates `choices`, using `Literal` also populates choices but does not set converter unlike `StrEnum`. Using a `re.Pattern` typehint automatically uses `re.compile` as a converter. A custom type-converter can be used by passing `converter=...` to either `option()` or `positional()`. Come common utility converters are defined in [converters.py](arcparse/converters.py).

Custom converters may be used in combination with multiple values per argument. These converters are called `itemwise` and need to be wrapped in `itemwise()`. This wrapper is used automatically if an argument is typed as `list[...]` and no converter is set.
```py
from arcparse.converters import sv, csv, sv_dict, itemwise
from enum import StrEnum
from re import Pattern
from typing import Literal

@arcparser
class Args:
    class Result(StrEnum):
        PASS = "pass"
        FAIL = "fail"

        @classmethod
        def from_int(cls, arg: str) -> "Result":
            number = int(arg)
            return cls.PASS if number == 1 else cls.FAIL

    number: int
    result: Result
    literal: Literal["yes", "no"]
    pattern: Pattern
    custom: Result = option(converter=Result.from_int)
    ints: list[int] = option(converter=csv(int))
    ip_parts: list[int] = option(converter=sv(".", int), name_override="ip")
    int_overrides: dict[str, int] = option(converter=sv_dict(",", "=", value_type=int))  # accepts x=1,y=2
    results: list[Result] = option(converter=itemwise(Result.from_int))
```

### dict helpers
Sometimes creating an argument able to choose a value from a dict by its key is desired. `dict_option()` and `dict_positional()` do exactly that. In the following example passing `--foo yes` will result in `.foo` being `True`.
```py
from arcparse import dict_option

values = {
    "yes": True,
    "no": False,
}

@arcparser
class Args:
    foo: bool = dict_option(values)
```

### Mutually exclusive groups
Use `mx_group` to group multiple arguments together in a mutually exclusive group. Each argument has to have a default defined either implicitly through the type (being `bool` or a union with `None`) or explicitly with `default`.
```py
@arcparser
class Args:
    group = mx_group()  # alternatively use `mx_group=(group := mx_group())` on the next line
    flag: bool = flag(mx_group=group)
    option: str | None = option(mx_group=group)
```

### Subparsers
Type-hinting an argument as a union of classes creates subparsers from them in the background. Assigning from `subparsers()` gives them names as they will be entered from the command-line. Subparsers are required by default. Adding `None` to the union makes the subparsers optional.
```py
class FooArgs:
    arg1: str

class BarArgs:
    arg2: int = positional()

@arcparser
class Args:
    action: FooArgs | BarArgs = subparsers("foo", "bar")

@arcparser
class OptionalSubparsersArgs:
    action: FooArgs | BarArgs | None = subparsers("foo", "bar")
```

Once the arguments are parsed, the different subparsers can be triggered and distinguished like so:
```shell
python3 script.py foo --arg1 baz
python3 script.py bar --arg2 123
```
```py
args = Args.parse("foo --arg1 baz".split())
if isinstance(foo := args.action, FooArgs):
    print(f"foo {foo.arg1}")
elif isinstance(bar := args.action, BarArgs):
    print(f"bar {bar.arg2}")
```

### Parser inheritance
Parsers can inherit arguments from other parsers. This is useful if there are common arguments among multiple subparsers. Note that current implementation disallows inheriting directly from classes already wrapped by `@arcparser`, inherit from `ClassAlreadySubparsered.shape` instead (if `Common` was wrapped in `@arcparser`, inherit from `Common.shape`).

```py
class Common:
    debug: bool

class FooArgs(Common):
    foo: bool

class BarArgs(Common):
    bar: bool

@arcparser
class Args:
    action: FooArgs | BarArgs = subparsers("foo", "bar")

args = Args.parse("foo --debug".split())
```

## Credits
This project was inspired by [swansonk14/typed-argument-parser](https://github.com/swansonk14/typed-argument-parser).

## Known issues

### Annotations
`from __future__ import annotations` makes all annotations strings at runtime. This library relies on class variable annotations's types being actual types. `inspect.get_annotations(obj, eval_str=True)` is used to evaluate string annotations to types in order to assign converters. If an argument is annotated with a non-builtin type which is defined outside of the argument-defining class body the type can't be found which results in `NameError`s. This is avoidable either by only using custom types which have been defined in the argument-defining class body (which is restrictive), or alternatively by not using the `annotations` import which should not be necessary from python 3.13 forward thanks to [PEP 649](https://peps.python.org/pep-0649/).

            

Raw data

            {
    "_id": null,
    "home_page": "https://github.com/Kuba314/arcparse",
    "name": "arcparse",
    "maintainer": "",
    "docs_url": null,
    "requires_python": ">=3.12,<4.0",
    "maintainer_email": "",
    "keywords": "argparse,declarative,argument,parsing,type-safe",
    "author": "Jakub Rozek",
    "author_email": "jakub.rozek314@gmail.com",
    "download_url": "https://files.pythonhosted.org/packages/1d/55/5136b31c9edb8b284f46880d203b85443b904983d073ac46bb59123ac835/arcparse-0.6.12.tar.gz",
    "platform": null,
    "description": "# arcparse\nDeclare program arguments type-safely.\n\nThis project builds on top of `argparse` by adding type-safety and allowing a more expressive argument parser definition.\n\nDisclaimer: This library is young and relatively unstable. Issues are open and pull requests are welcome!\n\n## Example usage\n```py\nfrom arcparse import arcparser, positional\n\n@arcparser\nclass Args:\n    name: str = positional()\n    age: int\n    happy: bool\n\n\nargs = Args.parse(\"--age 25 --happy Thomas\".split())\nprint(f\"Hi, my name is {args.name}!\")\n```\n\nFor a complete overview of features see [Features](#features).\n\n## Installation\n```shell\n# Using pip\n$ pip install arcparse\n```\n\n## Features\n\n### Required and optional arguments\nArguments without explicitly assigned argument class are implicitly options (prefixed with `--`). A non-optional typehint results in `required=True` for options. Defaults can be set by directly assigning them. You can use `option()` to further customize the argument.\n```py\n@arcparser\nclass Args:\n    required: str\n    optional: str | None\n    default: str = \"foo\"\n    default_with_help: str = option(default=\"bar\", help=\"help message\")\n```\n\n### Positional arguments\nPositional arguments use `positional()`. Optional type-hints use `nargs=\"?\"` in the background.\n```py\n@arcparser\nclass Args:\n    required: str = positional()\n    optional: str | None = positional()\n```\n\n### Flags\nAll arguments type-hinted as `bool` are flags, they use `action=\"store_true\"` in the background. Flags (as well as options) can also define short forms for each argument. They can also disable the long form with `short_only=True`.\n\nUse `no_flag()` to easily create a `--no-...` flag with `action=\"store_false\"`.\n\nUse `tri_flag()` (or type-hint argument as `bool | None`) to create a \"true\" flag and a \"false\" flag (e.g. `--clone` and `--no-clone`). Passing `--clone` will store `True`, passing `--no-clone` will store `False` and not passing anything will store `None`. Passing both is an error ensured by an implicit mutually exclusive group.\n```py\n@arcparser\nclass Args:\n    sync: bool\n    recurse: bool = no_flag(help=\"Do not recurse\")\n    clone: bool | None\n\n    debug: bool = flag(\"-d\")  # both -d and --debug\n    verbose: bool = flag(\"-v\", short_only=True)  # only -v\n```\n\n### Multiple values per argument\nBy type-hinting the argument as `list[...]`, the argument will use `nargs=\"*\"` in the background. Passing `at_least_one=True` uses `nargs=\"+\"` instead. Passing `append=True` to `option()` uses `action=\"append\"` instead (this is available only for `option()` and incompatible with `at_least_one`).\n```py\n@arcparser\nclass Args:\n    option_nargs: list[str]\n    positional_nargs: list[str] = positional()\n    append_option: list[str] = option(append=True)\n    nargs_plus_option: list[str] = option(at_least_one=True)\n    nargs_plus_positional: list[str] = positional(at_least_one=True)\n```\n\nNote that `option(at_least_one=True)` will cause the option to be required. If this is not intended, provide a default value.\n\n### Name overriding\nPassing `name_override=...` will cause the provided string to be used instead of the variable name for the argument name. The string will undergo a replacement of `_` with `-` and will contain a `--` prefix if used in `option()`.\n\nThis is useful in combination with accepting multiple values with `append=True`, because the user will use `--value foo --value bar`, while the code will use `args.values`.\n```py\n@arcparser\nclass Args:\n    values: list[str] = option(name_override=\"value\", append=True)\n```\n\n### Type conversions\nAutomatic type conversions are supported. The type-hint type is used to convert the string argument to the desired type. This is NOT done using argparse's `type=...` because it was causing issues for `dict_option()` and `dict_positional()`. Using a `StrEnum` subclass as a type-hint automatically populates `choices`, using `Literal` also populates choices but does not set converter unlike `StrEnum`. Using a `re.Pattern` typehint automatically uses `re.compile` as a converter. A custom type-converter can be used by passing `converter=...` to either `option()` or `positional()`. Come common utility converters are defined in [converters.py](arcparse/converters.py).\n\nCustom converters may be used in combination with multiple values per argument. These converters are called `itemwise` and need to be wrapped in `itemwise()`. This wrapper is used automatically if an argument is typed as `list[...]` and no converter is set.\n```py\nfrom arcparse.converters import sv, csv, sv_dict, itemwise\nfrom enum import StrEnum\nfrom re import Pattern\nfrom typing import Literal\n\n@arcparser\nclass Args:\n    class Result(StrEnum):\n        PASS = \"pass\"\n        FAIL = \"fail\"\n\n        @classmethod\n        def from_int(cls, arg: str) -> \"Result\":\n            number = int(arg)\n            return cls.PASS if number == 1 else cls.FAIL\n\n    number: int\n    result: Result\n    literal: Literal[\"yes\", \"no\"]\n    pattern: Pattern\n    custom: Result = option(converter=Result.from_int)\n    ints: list[int] = option(converter=csv(int))\n    ip_parts: list[int] = option(converter=sv(\".\", int), name_override=\"ip\")\n    int_overrides: dict[str, int] = option(converter=sv_dict(\",\", \"=\", value_type=int))  # accepts x=1,y=2\n    results: list[Result] = option(converter=itemwise(Result.from_int))\n```\n\n### dict helpers\nSometimes creating an argument able to choose a value from a dict by its key is desired. `dict_option()` and `dict_positional()` do exactly that. In the following example passing `--foo yes` will result in `.foo` being `True`.\n```py\nfrom arcparse import dict_option\n\nvalues = {\n    \"yes\": True,\n    \"no\": False,\n}\n\n@arcparser\nclass Args:\n    foo: bool = dict_option(values)\n```\n\n### Mutually exclusive groups\nUse `mx_group` to group multiple arguments together in a mutually exclusive group. Each argument has to have a default defined either implicitly through the type (being `bool` or a union with `None`) or explicitly with `default`.\n```py\n@arcparser\nclass Args:\n    group = mx_group()  # alternatively use `mx_group=(group := mx_group())` on the next line\n    flag: bool = flag(mx_group=group)\n    option: str | None = option(mx_group=group)\n```\n\n### Subparsers\nType-hinting an argument as a union of classes creates subparsers from them in the background. Assigning from `subparsers()` gives them names as they will be entered from the command-line. Subparsers are required by default. Adding `None` to the union makes the subparsers optional.\n```py\nclass FooArgs:\n    arg1: str\n\nclass BarArgs:\n    arg2: int = positional()\n\n@arcparser\nclass Args:\n    action: FooArgs | BarArgs = subparsers(\"foo\", \"bar\")\n\n@arcparser\nclass OptionalSubparsersArgs:\n    action: FooArgs | BarArgs | None = subparsers(\"foo\", \"bar\")\n```\n\nOnce the arguments are parsed, the different subparsers can be triggered and distinguished like so:\n```shell\npython3 script.py foo --arg1 baz\npython3 script.py bar --arg2 123\n```\n```py\nargs = Args.parse(\"foo --arg1 baz\".split())\nif isinstance(foo := args.action, FooArgs):\n    print(f\"foo {foo.arg1}\")\nelif isinstance(bar := args.action, BarArgs):\n    print(f\"bar {bar.arg2}\")\n```\n\n### Parser inheritance\nParsers can inherit arguments from other parsers. This is useful if there are common arguments among multiple subparsers. Note that current implementation disallows inheriting directly from classes already wrapped by `@arcparser`, inherit from `ClassAlreadySubparsered.shape` instead (if `Common` was wrapped in `@arcparser`, inherit from `Common.shape`).\n\n```py\nclass Common:\n    debug: bool\n\nclass FooArgs(Common):\n    foo: bool\n\nclass BarArgs(Common):\n    bar: bool\n\n@arcparser\nclass Args:\n    action: FooArgs | BarArgs = subparsers(\"foo\", \"bar\")\n\nargs = Args.parse(\"foo --debug\".split())\n```\n\n## Credits\nThis project was inspired by [swansonk14/typed-argument-parser](https://github.com/swansonk14/typed-argument-parser).\n\n## Known issues\n\n### Annotations\n`from __future__ import annotations` makes all annotations strings at runtime. This library relies on class variable annotations's types being actual types. `inspect.get_annotations(obj, eval_str=True)` is used to evaluate string annotations to types in order to assign converters. If an argument is annotated with a non-builtin type which is defined outside of the argument-defining class body the type can't be found which results in `NameError`s. This is avoidable either by only using custom types which have been defined in the argument-defining class body (which is restrictive), or alternatively by not using the `annotations` import which should not be necessary from python 3.13 forward thanks to [PEP 649](https://peps.python.org/pep-0649/).\n",
    "bugtrack_url": null,
    "license": "MIT",
    "summary": "Declare program arguments declaratively and type-safely",
    "version": "0.6.12",
    "project_urls": {
        "Homepage": "https://github.com/Kuba314/arcparse",
        "Repository": "https://github.com/Kuba314/arcparse"
    },
    "split_keywords": [
        "argparse",
        "declarative",
        "argument",
        "parsing",
        "type-safe"
    ],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "c6f72794f9a29b1ebdec1a7e5d1f9ad84718ae77636b74258c8a5faf76090e45",
                "md5": "8165f53ffd762fd22ef8b039d3df9b50",
                "sha256": "aac0e4515db66d36f58e336756c4d320790ece5bac8214ab3377282c692bd13a"
            },
            "downloads": -1,
            "filename": "arcparse-0.6.12-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "8165f53ffd762fd22ef8b039d3df9b50",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.12,<4.0",
            "size": 13938,
            "upload_time": "2024-03-17T10:07:55",
            "upload_time_iso_8601": "2024-03-17T10:07:55.060434Z",
            "url": "https://files.pythonhosted.org/packages/c6/f7/2794f9a29b1ebdec1a7e5d1f9ad84718ae77636b74258c8a5faf76090e45/arcparse-0.6.12-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "1d555136b31c9edb8b284f46880d203b85443b904983d073ac46bb59123ac835",
                "md5": "2794f47b01006ed8ff73c72c1507af82",
                "sha256": "2b8f99757ba4f8656d811b1323b12975f8bdca5512ecd261c513b861ed05bd42"
            },
            "downloads": -1,
            "filename": "arcparse-0.6.12.tar.gz",
            "has_sig": false,
            "md5_digest": "2794f47b01006ed8ff73c72c1507af82",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.12,<4.0",
            "size": 14199,
            "upload_time": "2024-03-17T10:07:57",
            "upload_time_iso_8601": "2024-03-17T10:07:57.008388Z",
            "url": "https://files.pythonhosted.org/packages/1d/55/5136b31c9edb8b284f46880d203b85443b904983d073ac46bb59123ac835/arcparse-0.6.12.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2024-03-17 10:07:57",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "Kuba314",
    "github_project": "arcparse",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": true,
    "lcname": "arcparse"
}
        
Elapsed time: 0.26821s