Skip to content

liran-funaro/classparse

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

35 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

classparse

Coverage Status

Declarative ArgumentParser definition with dataclass notation.

  • No ArgumentParser boilerplate code
  • IDE autocompletion and type hints

Install

pip install classparse==0.1.4

Simple Example

This is a simple example of the most basic usage of this library.

# examples/simple.py
from dataclasses import dataclass

from classparse import classparser

@classparser
@dataclass
class SimpleArgs:
    """My simple program's arguments"""

    retries: int = 5  # number of retries
    eps: float = 1e-3  # epsilon

if __name__ == "__main__":
    print(SimpleArgs.parse_args())
$ python examples/simple.py --help
usage: simple.py [-h] [--retries RETRIES] [--eps EPS]

My simple program's arguments

options:
  -h, --help         show this help message and exit
  --retries RETRIES  number of retries
  --eps EPS          epsilon
$ python examples/simple.py --retries 10 --eps 1e-6
SimpleArgs(retries=10, eps=1e-06)

Exhaustive Usage Example

This example demonstrates all the usage scenarios of this library.

# examples/usage.py
import dataclasses
from dataclasses import dataclass
from enum import Enum, auto
from pathlib import Path
from typing import List, Literal, Optional, Tuple, Union, Any

from classparse import arg, classparser, no_arg, pos_arg
from classparse.analyze import to_arg_name, to_var_name

class Action(Enum):
    Initialize = "init"
    Execute = "exec"

class Animal(Enum):
    Cat = auto()
    Dog = auto()

@dataclass(frozen=True)
class SubChild:
    """Double nested class"""

    str_arg: str = "sub-child-test"

@dataclass(frozen=True)
class Child:
    """Many nesting levels are supported"""

    str_arg: str = "child-test"  # (default=%(default)s)
    child_arg: SubChild = None  # We can override the nested class description

@classparser(
    # Keyword arguments are passed to the parser init.
    prog="my_program.py",
    # `default_argument_args` sets default arguments for each call of add_argument().
    default_argument_args=dict(help="(type: %(type)s)"),
)
@dataclass(frozen=True)
class AllOptions:
    """
    Class doc string ==> parser description.
    The fields' inline/above comment ==> argument's help.
    """

    pos_arg_1: str  # Field with no explicit default ==> positional arguments (default=%(default)s)
    pos_arg_2: int = pos_arg(
        5,
        nargs="?",
        help=(
            "pos_arg() is a wrapper around dataclasses.field()."
            "The first argument (optional) is the argument default (default=%(default)s)."
            "The following keyword arguments can be any argparse.add_argument() parameter."
        ),
    )  # When the help field is specified explicitly, the inline comment is ignored
    int_arg: int = 1  # Field's type and default are applied to the parser (type=%(type)s, default=%(default)s)
    str_enum_choice_arg: Action = Action.Initialize  # StrEnum ==> choice argument (type=%(type)s, default=%(default)s)
    int_enum_choice_arg: Animal = Animal.Cat  # IntEnum ==> choice argument (type=%(type)s, default=%(default)s)
    literal_arg: Literal["a", "b", "c"] = None  # Literal ==> choice argument (type=%(type)s, default=%(default)s)
    literal_int_arg: Literal[1, 2, 3] = None  # Literal's type is automatically inferred (type=%(type)s)
    mixed_literal: Literal[1, 2, "3", "4", True, Animal.Cat] = None  # We can mix multiple literal types (type=%(type)s)
    optional_arg: Optional[int] = None  # Optional can be used for type hinting (type=%(type)s)
    just_optional_arg: Optional = None  # Bare optional also works, although it is uninformative (type=%(type)s)
    any_optional_arg: Optional[Any] = None  # This is also uninformative (type=%(type)s)
    optional_choice_arg: Optional[Action] = None  # Nested types are supported (type=%(type)s)
    union_arg: Union[int, float, bool] = None  # Tries to convert to type in order until first success (type=%(type)s)
    path_arg: Path = None
    flag_arg: int = arg(
        "-f",
        help=(
            "arg() is a wrapper around dataclasses.field()."
            "The first argument (optional) is the short argument name."
            "The following keyword arguments can be any argparse.add_argument() parameter."
        ),
        default=1,
    )
    required_arg: float = arg("-r", required=True)  # E.g., required=%(required)s
    metavar_arg: str = arg(metavar="M")  # E.g., metavar=%(metavar)s
    int_list: List[int] = (1,)  # List[T] ==> nargs="+" (type=%(type)s)
    int_2_list: Tuple[int, int] = (1, 2)  # Tuple[T1, T2, Tn] ==> nargs=<tuple length> (nargs=%(nargs)s, type=%(type)s)
    int_tuple: Tuple[int, ...] = (1, 2, 3)  # Tuple[T, ...] ==> nargs="+" (nargs=%(nargs)s, type=%(type)s)
    multi_type_tuple: Tuple[int, float, str] = (1, 1e-3, "a")  # We can use multiple types (type=%(type)s)
    actions: List[Action] = ()  # List[Enum] ==> choices with nargs="+" (nargs=%(nargs)s, type=%(type)s)
    animals: List[Animal] = ()  # List[Enum] ==> choices with nargs="+" (nargs=%(nargs)s, type=%(type)s)
    literal_list: List[Literal["aa", "bb", 11, 22, Animal.Cat]] = ("aa",)  # List[Literal] ==> choices with nargs="+"
    union_list: List[Union[int, float, str, bool]] = ()
    union_with_literal: List[Union[Literal["a", "b", 1, 2], float, bool]] = ()
    typeless_list: list = ()  # If list type is unspecified, then it uses argparse default (type=%(type)s)
    typeless_typing_list: List = ()  # typing.List or list are supported
    none_bool_arg: bool = None  # bool ==> argparse.BooleanOptionalAction (type=%(type)s)
    true_bool_arg: bool = True  # We can set any default value
    false_bool_arg: bool = False
    complex_arg: complex = complex(1, -1)
    group_arg: Child = None
    default_child_arg: Child = Child(str_arg="override")  # We can override the nested class default values
    default_factory: List[int] = arg(default_factory=lambda: [1, 2, 3])  # Default factory=%(default)s

    # no_arg() is used to not include this argument in the parser.
    # The first argument (optional) sets the default value.
    # The following keyword arguments is forwarded to the dataclasses.field() method.
    non_parsed_arg: int = no_arg(0)
    non_parsed_with_meta: str = no_arg("", metadata={"key": "third-party-data"})

    # We used this argument for the README example.
    # Note that comments above the arg are also included in the help of the argument.
    # This is a convenient way to include long help messages.
    show: List[str] = arg("-s", default=())

    def __repr__(self):
        """Print only the specified fields"""
        fields = self.show or list(dataclasses.asdict(self))
        return "\n".join([f"{to_arg_name(k)}: {getattr(self, to_var_name(k))}" for k in fields])

if __name__ == "__main__":
    print(AllOptions.parse_args())
$ python examples/usage.py --help
usage: my_program.py [-h] [--int-arg INT_ARG]
                     [--str-enum-choice-arg {Initialize/init,Execute/exec}]
                     [--int-enum-choice-arg {Cat/1,Dog/2}]
                     [--literal-arg {a,b,c}] [--literal-int-arg {1,2,3}]
                     [--mixed-literal {1,2,3,4,True,Cat/1}]
                     [--optional-arg OPTIONAL_ARG]
                     [--just-optional-arg JUST_OPTIONAL_ARG]
                     [--any-optional-arg ANY_OPTIONAL_ARG]
                     [--optional-choice-arg {Initialize/init,Execute/exec}]
                     [--union-arg UNION_ARG] [--path-arg PATH_ARG]
                     [--flag-arg FLAG_ARG] --required-arg REQUIRED_ARG
                     [--metavar-arg M] [--int-list INT_LIST [INT_LIST ...]]
                     [--int-2-list INT_2_LIST INT_2_LIST]
                     [--int-tuple INT_TUPLE [INT_TUPLE ...]]
                     [--multi-type-tuple MULTI_TYPE_TUPLE MULTI_TYPE_TUPLE MULTI_TYPE_TUPLE]
                     [--actions {Initialize/init,Execute/exec} [{Initialize/init,Execute/exec} ...]]
                     [--animals {Cat/1,Dog/2} [{Cat/1,Dog/2} ...]]
                     [--literal-list {aa,bb,11,22,Cat/1} [{aa,bb,11,22,Cat/1} ...]]
                     [--union-list UNION_LIST [UNION_LIST ...]]
                     [--union-with-literal UNION_WITH_LITERAL [UNION_WITH_LITERAL ...]]
                     [--typeless-list TYPELESS_LIST [TYPELESS_LIST ...]]
                     [--typeless-typing-list TYPELESS_TYPING_LIST [TYPELESS_TYPING_LIST ...]]
                     [--none-bool-arg | --no-none-bool-arg]
                     [--true-bool-arg | --no-true-bool-arg]
                     [--false-bool-arg | --no-false-bool-arg]
                     [--complex-arg COMPLEX_ARG]
                     [--group-arg.str-arg GROUP_ARG.STR_ARG]
                     [--group-arg.child-arg.str-arg GROUP_ARG.CHILD_ARG.STR_ARG]
                     [--default-child-arg.str-arg DEFAULT_CHILD_ARG.STR_ARG]
                     [--default-child-arg.child-arg.str-arg DEFAULT_CHILD_ARG.CHILD_ARG.STR_ARG]
                     [--default-factory DEFAULT_FACTORY [DEFAULT_FACTORY ...]]
                     [--show SHOW [SHOW ...]]
                     pos-arg-1 [pos-arg-2]

Class doc string ==> parser description. The fields' inline/above comment ==>
argument's help.

positional arguments:
  pos-arg-1             Field with no explicit default ==> positional
                        arguments (default=None)
  pos-arg-2             pos_arg() is a wrapper around dataclasses.field().The
                        first argument (optional) is the argument default
                        (default=5).The following keyword arguments can be any
                        argparse.add_argument() parameter.

options:
  -h, --help            show this help message and exit
  --int-arg INT_ARG     Field's type and default are applied to the parser
                        (type=int, default=1)
  --str-enum-choice-arg {Initialize/init,Execute/exec}
                        StrEnum ==> choice argument (type=Action,
                        default=Initialize)
  --int-enum-choice-arg {Cat/1,Dog/2}
                        IntEnum ==> choice argument (type=Animal, default=Cat)
  --literal-arg {a,b,c}
                        Literal ==> choice argument (type=str, default=None)
  --literal-int-arg {1,2,3}
                        Literal's type is automatically inferred (type=int)
  --mixed-literal {1,2,3,4,True,Cat/1}
                        We can mix multiple literal types
                        (type=typing.Literal[1, 2, '3', '4', True,
                        <Animal.Cat: 1>])
  --optional-arg OPTIONAL_ARG
                        Optional can be used for type hinting (type=int)
  --just-optional-arg JUST_OPTIONAL_ARG
                        Bare optional also works, although it is uninformative
                        (type=None)
  --any-optional-arg ANY_OPTIONAL_ARG
                        This is also uninformative (type=None)
  --optional-choice-arg {Initialize/init,Execute/exec}
                        Nested types are supported (type=Action)
  --union-arg UNION_ARG
                        Tries to convert to type in order until first success
                        (type=typing.Union[int, float, bool])
  --path-arg PATH_ARG   (type: Path)
  --flag-arg FLAG_ARG, -f FLAG_ARG
                        arg() is a wrapper around dataclasses.field().The
                        first argument (optional) is the short argument
                        name.The following keyword arguments can be any
                        argparse.add_argument() parameter.
  --required-arg REQUIRED_ARG, -r REQUIRED_ARG
                        E.g., required=True
  --metavar-arg M       E.g., metavar=M
  --int-list INT_LIST [INT_LIST ...]
                        List[T] ==> nargs="+" (type=int)
  --int-2-list INT_2_LIST INT_2_LIST
                        Tuple[T1, T2, Tn] ==> nargs=<tuple length> (nargs=2,
                        type=int)
  --int-tuple INT_TUPLE [INT_TUPLE ...]
                        Tuple[T, ...] ==> nargs="+" (nargs=+, type=int)
  --multi-type-tuple MULTI_TYPE_TUPLE MULTI_TYPE_TUPLE MULTI_TYPE_TUPLE
                        We can use multiple types (type=typing.Union[int,
                        float, str])
  --actions {Initialize/init,Execute/exec} [{Initialize/init,Execute/exec} ...]
                        List[Enum] ==> choices with nargs="+" (nargs=+,
                        type=Action)
  --animals {Cat/1,Dog/2} [{Cat/1,Dog/2} ...]
                        List[Enum] ==> choices with nargs="+" (nargs=+,
                        type=Animal)
  --literal-list {aa,bb,11,22,Cat/1} [{aa,bb,11,22,Cat/1} ...]
                        List[Literal] ==> choices with nargs="+"
  --union-list UNION_LIST [UNION_LIST ...]
                        (type: typing.Union[int, float, str, bool])
  --union-with-literal UNION_WITH_LITERAL [UNION_WITH_LITERAL ...]
                        (type: typing.Union[typing.Literal['a', 'b', 1, 2],
                        float, bool])
  --typeless-list TYPELESS_LIST [TYPELESS_LIST ...]
                        If list type is unspecified, then it uses argparse
                        default (type=None)
  --typeless-typing-list TYPELESS_TYPING_LIST [TYPELESS_TYPING_LIST ...]
                        typing.List or list are supported
  --none-bool-arg, --no-none-bool-arg
                        bool ==> argparse.BooleanOptionalAction (type=bool)
  --true-bool-arg, --no-true-bool-arg
                        We can set any default value (default: True)
  --false-bool-arg, --no-false-bool-arg
                        (type: bool) (default: False)
  --complex-arg COMPLEX_ARG
                        (type: complex)
  --default-factory DEFAULT_FACTORY [DEFAULT_FACTORY ...]
                        Default factory=[1, 2, 3]
  --show SHOW [SHOW ...], -s SHOW [SHOW ...]
                        We used this argument for the README example. Note
                        that comments above the arg are also included in the
                        help of the argument. This is a convenient way to
                        include long help messages.

group-arg:
  Many nesting levels are supported

  --group-arg.str-arg GROUP_ARG.STR_ARG
                        (default=child-test)

child-arg:
  We can override the nested class description

  --group-arg.child-arg.str-arg GROUP_ARG.CHILD_ARG.STR_ARG
                        (type: str)

default-child-arg:
  We can override the nested class default values

  --default-child-arg.str-arg DEFAULT_CHILD_ARG.STR_ARG
                        (default=override)

child-arg:
  We can override the nested class description

  --default-child-arg.child-arg.str-arg DEFAULT_CHILD_ARG.CHILD_ARG.STR_ARG
                        (type: str)

Note that for Enums, we can use either the enum name or its value.

$ python examples/usage.py str-choices --actions Initialize init Execute exec -r1 -s actions
actions: [<Action.Initialize: 'init'>, <Action.Initialize: 'init'>, <Action.Execute: 'exec'>, <Action.Execute: 'exec'>]
$ python examples/usage.py int-choices --animals Cat 1 Dog 2 -r1 -s animals
animals: [<Animal.Cat: 1>, <Animal.Cat: 1>, <Animal.Dog: 2>, <Animal.Dog: 2>]

Loading Default Values From File

classparse supports loading default values from a YAML file.

# examples/load_defaults.py
from dataclasses import dataclass

from classparse import classparser

@classparser(load_defaults_from_file=True)
@dataclass
class SimpleLoadDefaults:
    """A simple dataclass that loads defaults from file"""

    retries: int = 5  # retries-default: %(default)s
    eps: float = 1e-3  # eps-default: %(default)s

if __name__ == "__main__":
    print(SimpleLoadDefaults.parse_args())
$ python examples/load_defaults.py --help
usage: load_defaults.py [-h] [--load-defaults PATH] [--retries RETRIES]
                        [--eps EPS]

A simple dataclass that loads defaults from file

options:
  -h, --help            show this help message and exit
  --load-defaults PATH  A YAML file path that overrides the default values.
  --retries RETRIES     retries-default: 5
  --eps EPS             eps-default: 0.001
$ printf "retries: 10\neps: 1e-10" | python examples/load_defaults.py --load-defaults - --eps 1e-6
SimpleLoadDefaults(retries=10, eps=1e-06)
  • Passing - to --load-defaults will load the YAML form stdin.

The help message will show the defaults after loading them from the YAML file.

$ printf "retries: 10\neps: 1e-10" | python examples/load_defaults.py --load-defaults - --help
usage: load_defaults.py [-h] [--load-defaults PATH] [--retries RETRIES]
                        [--eps EPS]

A simple dataclass that loads defaults from file

options:
  -h, --help            show this help message and exit
  --load-defaults PATH  A YAML file path that overrides the default values.
  --retries RETRIES     retries-default: 10
  --eps EPS             eps-default: 1e-10

Alternatives

  • Allow transforming dataclass to ArgumentParser.
  • Missing features:
    • Enum support
    • arg/pos_arg/no_arg functionality
    • Implicit positional argument
    • nargs support
  • Allow adding dataclass to ArgumentParser by using parser.add_arguments()
  • Requires boilerplate code to create the parser
  • Positional arguments
  • nargs support
  • Creating argument parser from classes and functions
  • Rich functionality
  • Post-processing of arguments
  • Save/load arguments
  • Load from dict

License

BSD-3

About

Defining argument parser using Python's dataclass.

Resources

License

Code of conduct

Stars

Watchers

Forks

Packages

No packages published

Languages