diff --git a/README.md b/README.md index 53efea6..0226260 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ simplifying args jargon +![[license](LICENSE)](https://img.shields.io/github/license/silvncr/jarguments) ![[publish status](https://github.com/silvncr/jarguments/actions/workflows/python-publish.yml)](https://img.shields.io/github/actions/workflow/status/silvncr/jarguments/python-publish.yml) ![[latest release](https://github.com/silvncr/jarguments/releases/latest)](https://img.shields.io/github/v/release/silvncr/jarguments) @@ -24,9 +25,7 @@ Providing a straightforward way to create command-line arguments. ## Installation ```sh -git clone https://github.com/silvncr/jarguments.git -cd jarguments -python setup.py install +pip install jarguments ``` ## Usage @@ -38,23 +37,25 @@ There are three steps to using the jarguments library: 1. Import the jarguments library. ```py - from jarguments import * + import jarguments as j ``` 2. Provide your arguments with jarguments' classes. ```py - args = JParser( - JBool('show_text', help='determines whether "text" is shown'), - JStr('text'), + args = j.JParser( + j.JBool('show-text', help='determines whether "text" is shown'), + j.JInt('number', default=1), + j.JArgument('text', type=str, multiple=True), ) ``` -3. Use your outputs; they are parsed automatically. +3. Use the outputs; they're parsed automatically! ```py if args.show_text: - print(args.text) + for _ in range(args.number): + print(args.text) ``` ### Command-line @@ -62,26 +63,27 @@ There are three steps to using the jarguments library: - Now you can run your script with arguments: ```sh - $ python script.py --show_text --text "hello world" - hello world + $ python example.py --show-text --text "hello" "world" + ["hello", "world"] ``` - Arguments without a default value are required. If you don't provide them, the script will raise an error: ```sh - $ python script.py --show_text + $ python example.py --show-text error: the following arguments are required: --text ``` - If you want to see help messages, run your script with the `-h` or `--help` flag: ```sh - $ python script.py -h - usage: script.py [-h] [--show_text] --text TEXT + $ python example.py -h + usage: example.py [-h] [--show-text [SHOW_TEXT]] [--number NUMBER] --text [TEXT ...] options: - -h, --help show this help message and exit - --show_text [SHOW_TEXT] + -h, --help show this help message and exit + --show-text [SHOW_TEXT] determines whether "text" is shown - --text TEXT + --number NUMBER + --text [TEXT ...] ``` diff --git a/jarguments/__init__.py b/jarguments/__init__.py index b1028e4..25d219e 100644 --- a/jarguments/__init__.py +++ b/jarguments/__init__.py @@ -2,31 +2,63 @@ Providing a straightforward way to create command-line arguments. ''' + # imports from argparse import Action, ArgumentParser, Namespace from typing import Any, Optional, Type, Union + # module info __author__ = 'silvncr' __license__ = 'MIT' __module_name__ = 'jarguments' __python_version__ = '3.6' -__version__ = '0.0.1' +__version__ = '0.1.0' + +# parse argument name +def parse_argument_name(name: str) -> Optional[str]: + ''' + Used to parse an argument name from a string. + ''' + for i in ['--', '-', '/']: + if name.startswith(i): + name = name[len(i):] + _name = name = name.strip().lower().replace(' ', '-').replace('_', '-') + for char in _name: + if not char.isalnum() and char not in ['-', '_']: + name = name.replace(char, '') + return name or None -# base argument + +# base argument class class JArgument: + ''' + Base argument class. + ''' def __init__( self, name: str, - type: Type[Union[bool, str]], + type: Type[Union[bool, int, str]], default: Union[Any, Optional[bool]] = None, short_name: Optional[str] = None, multiple: Optional[bool] = False, help: Optional[str] = None, ): - self.name = name - self.short_name = short_name or None + if not name: + raise ValueError(f'invalid argument name: "{name}"; \ + must not be empty') + if not name[0].isalpha(): + raise ValueError(f'invalid argument name: "{name}"; \ + must start with a letter') + elif name: + if _name := parse_argument_name(name): + self.name = _name + self.python_name = _name.replace('-', '_') + else: + raise ValueError(f'invalid argument name: "{name}"; \ + an error occured while processing the name') + self.short_name = parse_argument_name(short_name) if short_name else None self.type = type self.default = default self.required = default is None @@ -42,6 +74,9 @@ def __init__( # boolean argument class JBool(JArgument): + ''' + Boolean argument class. Extends `JArgument`. + ''' def __init__( self, name: str, @@ -59,8 +94,33 @@ def __init__( ) +# integer argument +class JInt(JArgument): + ''' + Integer argument class. Extends `JArgument`. + ''' + def __init__( + self, + name: str, + short_name: Optional[str] = None, + default: Optional[int] = None, + help: Optional[str] = None, + ): + super().__init__( + name, + int, + default, + short_name or None, + False, + help, + ) + + # string argument class JStr(JArgument): + ''' + String argument class. Extends `JArgument`. + ''' def __init__( self, name: str, @@ -78,8 +138,43 @@ def __init__( ) -# boolean argument parser -class JBooleanAction(Action): +# base argument action +class JAction(Action): + ''' + Base argument parser. + ''' + def __init__( + self, + option_strings, + dest, + nargs=None, + const=None, + default=None, + type=None, + choices=None, + required=False, + help=None, + metavar=None, + ): + super(JAction, self).__init__( + option_strings, + dest, + nargs=nargs, + const=const, + default=default, + type=type, + choices=choices, + required=required, + help=help, + metavar=metavar, + ) + + +# boolean argument action +class JBoolAction(JAction): + ''' + Boolean argument parser. Extends `JAction`. + ''' def __init__( self, option_strings, @@ -93,13 +188,13 @@ def __init__( help=None, metavar=None, ): - super(JBooleanAction, self).__init__( + super(JBoolAction, self).__init__( option_strings, dest, nargs=nargs, - const=const, + const=const, # type: ignore default=default, - type=type, + type=type, # type: ignore choices=choices, required=required, help=help, @@ -122,21 +217,56 @@ def __call__( ) -# base parser +# integer argument action +class JIntAction(JAction): + ''' + Integer argument parser. Extends `JAction`. + ''' + def __call__(self, parser, namespace, values, option_string=None): + try: + setattr(namespace, self.dest, int(str(values))) + except ValueError: + parser.error(f"Invalid integer value: {values}") + + +# string argument action +class JStrAction(JAction): + ''' + String argument parser. Extends `JAction`. + ''' + def __call__(self, parser, namespace, values, option_string=None): + setattr(namespace, self.dest, str(values)) + + +# parser class JParser(ArgumentParser): + ''' + Parser for instances of `JArgument` and its extensions. + ''' def __init__(self, *args: JArgument): super().__init__() for arg in args: args_dict = { - 'name': (f'--{arg.name}',), 'default': arg.default, + 'dest': arg.python_name, 'help': arg.help, } if arg.type == bool: args_dict = { **args_dict, **{ - 'action': JBooleanAction, - 'dest': arg.name, + 'action': JBoolAction, + } + } + elif arg.type == int: + args_dict = { + **args_dict, **{ + 'action': JIntAction, + } + } + elif arg.type == str: + args_dict = { + **args_dict, **{ + 'action': JStrAction, } } else: @@ -148,9 +278,28 @@ def __init__(self, *args: JArgument): } } if arg.short_name: - args_dict['name'] = (f'--{arg.name}', f'-{arg.short_name}') + args_dict = { + **args_dict, **{ + 'name': (f'--{arg.name}', f'-{arg.short_name}'), + } + } + else: + args_dict = { + **args_dict, **{ + 'name': (f'--{arg.name}',), + } + } self.add_argument(*args_dict.pop('name'), **args_dict) + # parse arguments + def parse_args(self, *args, **kwargs) -> Namespace: + namespace = super().parse_args(*args, **kwargs) + args_dict = vars(namespace) + args_obj = Namespace() + for python_name, value in args_dict.items(): + setattr(args_obj, python_name, value) + return args_obj + # return parsed arguments def __new__(cls, *args: JArgument) -> Namespace: instance = super().__new__(cls) diff --git a/jarguments/tests/example.py b/jarguments/tests/example.py index b6589f4..68f6efc 100644 --- a/jarguments/tests/example.py +++ b/jarguments/tests/example.py @@ -1,9 +1,11 @@ -from jarguments import JParser, JBool, JStr +import jarguments as j -args = JParser( - JBool('show_text', help='determines whether "text" is shown'), - JStr('text'), +args = j.JParser( + j.JBool('show-text', help='determines whether "text" is shown'), + j.JInt('number', default=1), + j.JArgument('text', type=str, multiple=True), ) if args.show_text: - print(args.text) + for _ in range(args.number): + print(args.text)