{"id":19867288,"url":"https://github.com/huzecong/argtyped","last_synced_at":"2025-05-02T06:31:01.651Z","repository":{"id":42109182,"uuid":"239037857","full_name":"huzecong/argtyped","owner":"huzecong","description":"Command line argument parser, with types.","archived":false,"fork":false,"pushed_at":"2022-07-07T09:17:47.000Z","size":87,"stargazers_count":25,"open_issues_count":1,"forks_count":2,"subscribers_count":2,"default_branch":"master","last_synced_at":"2024-08-10T19:04:52.184Z","etag":null,"topics":["argument-parsers","command-line","python3","typed-python"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/huzecong.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2020-02-07T23:03:52.000Z","updated_at":"2024-01-03T14:16:52.000Z","dependencies_parsed_at":"2022-09-09T22:23:18.510Z","dependency_job_id":null,"html_url":"https://github.com/huzecong/argtyped","commit_stats":null,"previous_names":[],"tags_count":5,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/huzecong%2Fargtyped","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/huzecong%2Fargtyped/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/huzecong%2Fargtyped/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/huzecong%2Fargtyped/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/huzecong","download_url":"https://codeload.github.com/huzecong/argtyped/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":224300085,"owners_count":17288677,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["argument-parsers","command-line","python3","typed-python"],"created_at":"2024-11-12T15:28:56.617Z","updated_at":"2024-11-12T15:28:57.386Z","avatar_url":"https://github.com/huzecong.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# `argtyped`: Command Line Argument Parser, with Types\n\n[![Build Status](https://github.com/huzecong/argtyped/workflows/Build/badge.svg)](https://github.com/huzecong/argtyped/actions?query=workflow%3ABuild+branch%3Amaster)\n[![CodeCov](https://codecov.io/gh/huzecong/argtyped/branch/master/graph/badge.svg?token=ELHfYJ2Ydq)](https://codecov.io/gh/huzecong/argtyped)\n[![PyPI](https://img.shields.io/pypi/v/argtyped.svg)](https://pypi.org/project/argtyped/)\n[![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/huzecong/argtyped/blob/master/LICENSE)\n\n`argtyped` is an command line argument parser with that relies on type annotations. It is built on\n[`argparse`](https://docs.python.org/3/library/argparse.html), the command line argument parser library built into\nPython. Compared with `argparse`, this library gives you:\n\n- More concise and intuitive syntax, less boilerplate code.\n- Type checking and IDE auto-completion for command line arguments.\n- A drop-in replacement for `argparse` in most cases.\n\nSince v0.4.0, `argtyped` also supports parsing arguments defined with an [attrs](https://attrs.org/)-class. See\n[Attrs Support](#attrs-support-new) for more details.  \n\n\n## Installation\n\nInstall stable release from [PyPI](https://pypi.org/project/argtyped/):\n```bash\npip install argtyped\n```\n\nOr, install the latest commit from GitHub:\n```bash\npip install git+https://github.com/huzecong/argtyped.git\n```\n\n## Usage\n\nWith `argtyped`, you can define command line arguments in a syntax similar to\n[`typing.NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple). The syntax is intuitive and can\nbe illustrated with an example:\n```python\nfrom typing import Optional\nfrom typing_extensions import Literal  # or directly import from `typing` in Python 3.8+\n\nfrom argtyped import Arguments, Switch\nfrom argtyped import Enum, auto\n\nclass LoggingLevels(Enum):\n    Debug = auto()\n    Info = auto()\n    Warning = auto()\n    Error = auto()\n    Critical = auto()\n\nclass MyArguments(Arguments):\n    model_name: str         # required argument of `str` type\n    hidden_size: int = 512  # `int` argument with default value of 512\n\n    activation: Literal[\"relu\", \"tanh\", \"sigmoid\"] = \"relu\"  # argument with limited choices\n    logging_level: LoggingLevels = LoggingLevels.Info        # using `Enum` class as choices\n\n    use_dropout: Switch = True  # switch argument, enable with \"--use-dropout\" and disable with \"--no-use-dropout\"\n    dropout_prob: Optional[float] = 0.5  # optional argument, \"--dropout-prob=none\" parses into `None`\n\nargs = MyArguments()\n```\n\nThis is equivalent to the following code with Python built-in `argparse`:\n```python\nimport argparse\nfrom enum import Enum\n\nclass LoggingLevels(Enum):\n    Debug = \"debug\"\n    Info = \"info\"\n    Warning = \"warning\"\n    Error = \"error\"\n    Critical = \"critical\"\n\nparser = argparse.ArgumentParser()\n\nparser.add_argument(\"--model-name\", type=str, required=True)\nparser.add_argument(\"--hidden-size\", type=int, default=512)\n\nparser.add_argument(\"--activation\", choices=[\"relu\", \"tanh\", \"sigmoid\"], default=\"relu\")\nparser.add_argument(\"--logging-level\", choices=list(LoggingLevels), type=LoggingLevels, default=\"info\")\n\nparser.add_argument(\"--use-dropout\", action=\"store_true\", dest=\"use_dropout\", default=True)\nparser.add_argument(\"--no-use-dropout\", action=\"store_false\", dest=\"use_dropout\")\nparser.add_argument(\"--dropout-prob\", type=lambda s: None if s.lower() == 'none' else float(s), default=0.5)\n\nargs = parser.parse_args()\n```\n\nSave the code into a file named `main.py`. Suppose the following arguments are provided:\n```bash\npython main.py \\\n    --model-name LSTM \\\n    --activation sigmoid \\\n    --logging-level debug \\\n    --no-use-dropout \\\n    --dropout-prob none\n```\nThen the parsed arguments will be equivalent to the following structure returned by `argparse`:\n```python\nargparse.Namespace(\n    model_name=\"LSTM\", hidden_size=512, activation=\"sigmoid\", logging_level=\"debug\",\n    use_dropout=False, dropout_prob=None)\n```\n\nArguments can also be pretty-printed:\n```\n\u003e\u003e\u003e print(args)\n\u003cclass '__main__.MyArguments'\u003e\n╔═════════════════╤══════════════════════════════════╗\n║ Arguments       │ Values                           ║\n╠═════════════════╪══════════════════════════════════╣\n║ model_name      │ 'LSTM'                           ║\n║ hidden_size     │ 512                              ║\n║ activation      │ 'sigmoid'                        ║\n║ logging_level   │ \u003cMyLoggingLevels.Debug: 'debug'\u003e ║\n║ use_dropout     │ False                            ║\n║ dropout_prob    │ None                             ║\n║ label_smoothing │ 0.1                              ║\n║ some_true_arg   │ True                             ║\n║ some_false_arg  │ False                            ║\n╚═════════════════╧══════════════════════════════════╝\n```\nIt is recommended though to use the `args.to_string()` method, which gives you control of the table width.\n\n## Attrs Support (New)\n\nThe way we define the arguments is very similar to defining a [dataclass](https://docs.python.org/3/library/dataclasses.html)\nor an [attrs](https://attrs.org)-class, so it seems natural to just write an attrs-class, and add parsing support to it.\n\nTo use `argtyped` with `attrs`, simply define an attrs-class as usual, and have it subclass `AttrsArguments`. Here's \nthe same example above, but implemented with attrs-classes, and with some bells and whistles:\n```python\nimport attr  # note: new style `attrs` syntax is also supported\nfrom argtyped import AttrsArguments\n\ndef _convert_logging_level(s: str) -\u003e LoggingLevels:\n    # Custom conversion function that takes the raw string value from the command line.\n    return LoggingLevels[s.lower()]\n\n@attr.s(auto_attribs=True)\nclass MyArguments(AttrsArguments):\n    model_name: str = attr.ib(metadata={\"positional\": True})  # positional argument\n    # Or: `model_name: str = argtyped.positional_arg()`.\n    layer_sizes: List[int] = attr.ib(metadata={\"nargs\": \"+\"})  # other metadata are treated as `argparse` options\n\n    activation: Literal[\"relu\", \"tanh\", \"sigmoid\"] = \"relu\"\n    logging_level: LoggingLevels = attr.ib(default=LoggingLevels.Info, converter=_convert_logging_level)\n\n    use_dropout: Switch = True\n    dropout_prob: Optional[float] = 0.5\n\n    _activation_fn: Callable[[float], float] = attr.ib(init=False)  # `init=False` attributes are not parsed\n\n    @dropout_prob.validator  # validators still work as you would expect\n    def _dropout_prob_validator(self, attribute, value):\n        if not 0.0 \u003c= value \u003c= 1.0:\n            raise ValueError(f\"Invalid probability {value}\")\n\n    @_activation_fn.default\n    def _activation_fn(self):\n        return _ACTIVATION_FNS[self.activation]\n```\n\nA few things to note here:\n- You can define positional arguments by adding `\"positional\": True` as metadata. If this feels unnatural, you could \n  also use `argtyped.positional_arg()`, which takes the same arguments as `attr.ib`.\n- You can pass additional options to `ArgumentParser.add_argument` by listing them as metadata as well. Note that \n  these options take precedence over `argtyped`'s computed arguments, for example, sequence arguments (`List[T]`) by \n  default uses `nargs=\"*\"`, but you could override it with metadata.\n- Attributes with custom converters will not be parsed; its converter will be called with the raw string value from\n  command line. If the attribute also has a default value, you should make sure that your converter works with both\n  strings and the default value.\n- `init=False` attributes are not treated as arguments, but they can be useful for storing computed values based on\n  arguments.\n- The default value logic is the same as normal attrs classes, and thus could be different from non-attrs `argtyped` \n  classes. For example, optional arguments are not considered to have an implicit default of `None`, and no type \n  validation is performed on default values.\n\nHere are the (current) differences between an attrs-based arguments class (`AttrsArguments`) versus the normal arguments\nclass (`Arguments`):\n- `AttrsArguments` supports positional arguments and custom options via metadata.\n- `AttrsArguments` handles default values with attrs, so there's no validation of default value types.  This also \n  means that nullable arguments must have an explicit default value of `None`, otherwise it becomes a required \n  argument.\n- `AttrsArguments` does not support `underscore=True`.\n- `AttrsArguments` does not have `to_dict`, `to_string` methods.\n- `AttrsArguments` needs to be called with the factory `parse_args` method to parse, while `Arguments` parses command \n  line arguments on construction.\n\n\n## Reference\n\n### The `argtyped.Arguments` Class\n\nThe `argtyped.Arguments` class is main class of the package, from which you should derive your custom class that holds\narguments. Each argument takes the form of a class attribute, with its type annotation and an optional default value.\n\nWhen an instance of your custom class is initialized, the command line arguments are parsed from `sys.argv` into values\nwith your annotated types. You can also provide the list of strings to parse by passing them as the parameter.\n\nThe parsed arguments are stored in an object of your custom type. This gives you arguments that can be auto-completed\nby the IDE, and type-checked by a static type checker like [mypy](http://mypy-lang.org/).\n\nThe following example illustrates the keypoints:\n```python\nclass MyArgs(argtyped.Arguments):\n    # name: type [= default_val]\n    value: int = 0\n\nargs = MyArgs()                    # equivalent to `parser.parse_args()`\nargs = MyArgs([\"--value\", \"123\"])  # equivalent to `parser.parse_args([\"--value\", \"123\"])\nassert isinstance(args, MyArgs)\n```\n\n#### `Arguments.to_dict(self)`\n\nConvert the set of arguments to a dictionary (`OrderedDict`).\n\n#### `Arguments.to_string(self, width: Optional[int] = None, max_width: Optional[int] = None)`\n\nRepresent the arguments as a table.\n- `width`: Width of the printed table. Defaults to `None`, which fits the table to its contents. An exception is raised\n  when the table cannot be drawn with the given width.\n- `max_width`: Maximum width of the printed table. Defaults to `None`, meaning no limits. Must be `None` if `width` is\n  not `None`.\n\n#### `argtyped.argument_specs`\n\nReturn a dictionary mapping argument names to their specifications, represented as the `argtyped.ArgumentSpec` type.\nThis is useful for programmatically accessing the list of arguments.\n\n### Argument Types\n\nTo summarize, whatever works for `argparse` works here. The following types are supported:\n\n- **Built-in types** such as `int`, `float`, `str`.\n- **Boolean type** `bool`. Accepted values (case-insensitive) for `True` are: `y`, `yes`, `true`, `ok`; accepted values\n  for `False` are: `n`, `no`, `false`.\n- **Choice types** `Literal[...]`. A choice argument is essentially an `str` argument with limited\n  choice of values. The ellipses can be filled with a tuple of `str`s, or an expression that evaluates to a list of\n  `str`s:\n  ```python\n  from argtyped import Arguments\n  from typing_extensions import Literal\n\n  class MyArgs(Arguments):\n      foo: Literal[\"debug\", \"info\", \"warning\", \"error\"]  # 4 choices\n\n  # argv: [\"--foo=debug\"] =\u003e foo=\"debug\"\n  ```\n  This is equivalent to the `choices` keyword in `argparse.add_argument`.\n  \n  **Note:** The choice type was previously named `Choices`. This is deprecated in favor of the\n  [`Literal` type](https://mypy.readthedocs.io/en/stable/literal_types.html) introduced in Python 3.8 and back-ported to\n  3.6 and 3.7 in the `typing_extensions` library. `Choices` was removed since version 0.4.0.\n- **Enum types** derived from `enum.Enum`. It is recommended to use `argtyped.Enum` which uses the instance names as\n  values:\n  ```python\n  from argtyped import Enum\n\n  class MyEnum(Enum):\n      Debug = auto()    # \"debug\"\n      Info = auto()     # \"info\"\n      Warning = auto()  # \"warning\"\n  ```\n- **Switch types** `Switch`. `Switch` arguments are like `bool` arguments, but they don't take values. Instead, a switch\n  argument `switch` requires `--switch` to enable and `--no-switch` to disable:\n  ```python\n  from argtyped import Arguments, Switch\n\n  class MyArgs(Arguments):\n      switch: Switch = True\n      bool_arg: bool = False\n\n  # argv: []                                 =\u003e flag=True,  bool_arg=False\n  # argv: [\"--switch\", \"--bool-arg=false\"]   =\u003e flag=True,  bool_arg=False\n  # argv: [\"--no-switch\", \"--bool-arg=true\"] =\u003e flag=False, bool_arg=True\n  # argv: [\"--switch=false\"]                 =\u003e WRONG\n  # argv: [\"--no-bool-arg\"]                  =\u003e WRONG\n  ```\n- **List types** `List[T]`, where `T` is any supported type except switch types. List arguments allow passing multiple\n  values on the command line following the argument flag, it is equivalent to setting `nargs=\"*\"` in `argparse`.\n  \n  Although there is no built-in support for other `nargs` settings such as `\"+\"` (one or more) or `N` (fixed number),\n  you can add custom validation logic by overriding the `__init__` method in your `Arguments` subclass.\n- **Optional types** `Optional[T]`, where `T` is any supported type except list or switch types. An optional argument\n  will be filled with `None` if no value is provided. It could also be explicitly set to `None` by using `none` as value\n  in the command line:\n  ```python\n  from argtyped import Arguments\n  from typing import Optional\n\n  class MyArgs(Arguments):\n      opt_arg: Optional[int]  # implicitly defaults to `None`\n\n  # argv: []                 =\u003e opt_arg=None\n  # argv: [\"--opt-arg=1\"]    =\u003e opt_arg=1\n  # argv: [\"--opt-arg=none\"] =\u003e opt_arg=None\n  ```\n- Any other type that takes a single `str` as `__init__` parameters. It is also theoretically possible to use a function\n  that takes an `str` as input, but it's not recommended as it's not type-safe.\n  \n## Composing `Arguments` Classes\n\nYou can split your arguments into separate `Arguments` classes and then compose them together by inheritance. A subclass\nwill have the union of all arguments in its base classes. If the subclass contains an argument with the same name as an\nargument in a base class, then the subclass definition takes precedence. For example:\n\n```python\nclass BaseArgs(Arguments):\n    a: int = 1\n    b: Switch = True\n\nclass DerivedArgs(BaseArgs):\n    b: str\n\n# args = DerivedArgs([])  # bad; `b` is required\nargs = DerivedArgs([\"--b=1234\"])\n```\n\n**Caveat:** For simplicity, we do not completely follow the [C3 linearization algorithm](\nhttps://en.wikipedia.org/wiki/C3_linearization) that determines the class MRO in Python. Thus, it is a bad idea to have\noverridden arguments in cases where there's diamond inheritance.\n\nIf you don't understand the above, that's fine. Just note that generally, it's a bad idea to have too complicated\ninheritance relationships with overridden arguments.\n\n## Argument Naming Styles\n\nBy default `argtyped` uses `--kebab-case` (with hyphens connecting words), which is the convention for UNIX command line\ntools. However, many existing tools use the awkward `--snake_case` (with underscores connecting words), and sometimes\nconsistency is preferred over aesthetics. If you want to use underscores, you can do so by setting `underscore=True`\ninside the parentheses where you specify base classes, like this:\n\n```python\nclass UnderscoreArgs(Arguments, underscore=True):\n    underscore_arg: int\n    underscore_switch: Switch = True\n\nargs = UnderscoreArgs([\"--underscore_arg\", \"1\", \"--no_underscore_switch\"])\n```\n\nThe underscore settings only affect arguments defined in the class scope; (non-overridden) inherited arguments are not\naffects. Thus, you can mix-and-match `snake_case` and `kebab-case` arguments:\n\n```python\nclass MyArgs(UnderscoreArgs):\n    kebab_arg: str\n\nclass MyFinalArgs(MyArgs, underscore=True):\n    new_underscore_arg: float\n\nargs = MyArgs([\"--underscore_arg\", \"1\", \"--kebab-arg\", \"kebab\", \"--new_underscore_arg\", \"1.0\"])\n```\n\n## Notes\n\n- Advanced `argparse` features such as subparsers, groups, argument lists, and custom actions are not supported.\n- Using switch arguments may result in name clashes: if a switch argument has name `arg`, there can be no argument with\n  the name `no_arg`.\n- Optional types:\n  - `Optional` can be used with `Literal`:\n    ```python\n    from argtyped import Arguments\n    from typing import Literal, Optional\n    \n    class MyArgs(Arguments):\n        foo: Optional[Literal[\"a\", \"b\"]]  # valid\n        bar: Literal[\"a\", \"b\", \"none\"]    # also works but is less obvious\n    ```\n  - `Optional[str]` would parse a value of `\"none\"` (case-insensitive) into `None`.\n- List types:\n  - `List[Optional[T]]` is a valid type. For example:\n    ```python\n    from argtyped import Arguments\n    from typing import List, Literal, Optional\n    \n    class MyArgs(Arguments):\n        foo: List[Optional[Literal[\"a\", \"b\"]]] = [\"a\", None, \"b\"]  # valid\n    \n    # argv: [\"--foo\", \"a\", \"b\", \"none\", \"a\", \"b\"] =\u003e foo=[\"a\", \"b\", None, \"a\", \"b\"]\n    ```\n  - List types cannot be nested inside a list or an optional type. Types such as `Optional[List[int]]` and\n    `List[List[int]]` are not accepted.\n\n## Under the Hood\n\nThis is what happens under the hood:\n1. When a subclass of `argtyped.Arguments` is constructed, type annotations and class-level attributes (i.e., the\n   default values) are collected to form argument declarations.\n2. After verifying the validity of declared arguments, `argtyped.ArgumentSpec` are created for each argument and stored\n   within the subclass as the `__arguments__` class attribute.\n3. When an instance of the subclass is initialized, if it's the first time, an instance of `argparse.ArgumentParser` is\n   created and arguments are registered with the parser. The parser is cached in the subclass as the `__parser__`\n   attribute.\n4. The parser's `parse_args` method is invoked with either `sys.argv` or strings provided as parameters, returning\n   parsed arguments.\n5. The parsed arguments are assigned to `self` (the instance of the `Arguments` subclass being initialized).\n\n## Todo\n\n- [ ] Support `action=\"append\"` or `action=\"extend\"` for `List[T]` types.\n  - Technically this is not a problem, but there's no elegant way to configure whether this behavior is desired.\n- [ ] Throw (suppressible) warnings on using non-type callables as types.\n- [ ] Support converting an `attrs` class into `Arguments`.\n- [ ] Support forward references in type annotations.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhuzecong%2Fargtyped","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fhuzecong%2Fargtyped","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhuzecong%2Fargtyped/lists"}