py-arguments 1.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,153 @@
1
+ Metadata-Version: 2.4
2
+ Name: py-arguments
3
+ Version: 1.0.1
4
+ Description-Content-Type: text/markdown
5
+ Provides-Extra: development
6
+ Requires-Dist: pre-commit; extra == "development"
7
+
8
+ # PyArg
9
+
10
+ A small layer on top of `argparse` to build CLIs with sub-commands by **reusing parser classes**.
11
+
12
+ Each parser is a class. You define its arguments in one method, and then plug that class into another parser as a
13
+ command, without rewriting the arguments.
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ pip install .
19
+ ```
20
+
21
+ For development (changes apply without reinstalling):
22
+
23
+ ```bash
24
+ pip install -e .
25
+ ```
26
+
27
+ Requires Python 3.10+ and has no dependencies.
28
+
29
+ ## Quick start
30
+
31
+ Create a class that inherits from `PyArg` and define the arguments in `setup_arguments`:
32
+
33
+ ```python
34
+ from pyarg import PyArg
35
+
36
+
37
+ class Login(PyArg):
38
+ def setup_arguments(self) -> None:
39
+ self.add_argument("--username", "-u")
40
+ self.add_argument("--password", "-p")
41
+
42
+
43
+ parser = Login(name="Login")
44
+ print(parser.get_arguments())
45
+ ```
46
+
47
+ ```console
48
+ $ python main.py -u ana -p 123
49
+ {'username': 'ana', 'password': '123'}
50
+ ```
51
+
52
+ Since `PyArg` inherits from `argparse.ArgumentParser`, everything from `argparse` still works
53
+ (`add_argument`, `add_mutually_exclusive_group`, `type`, `nargs`, `choices`, etc.).
54
+
55
+ ## Sub-commands
56
+
57
+ Use `add_command_parser` to attach parsers to a main one. Each command is named after the parser `name`, converted to
58
+ lowercase with spaces replaced by `-` (`"Make Directory"` becomes `make-directory`).
59
+
60
+ ```python
61
+ from pyarg import PyArg
62
+
63
+
64
+ class Login(PyArg):
65
+ def setup_arguments(self) -> None:
66
+ self.add_argument("--username", "-u")
67
+ self.add_argument("--password", "-p")
68
+
69
+
70
+ class Logout(PyArg):
71
+ def setup_arguments(self) -> None:
72
+ self.add_argument("--all", action="store_true")
73
+
74
+
75
+ class App(PyArg):
76
+ def setup_arguments(self) -> None:
77
+ pass # The main parser can have its own arguments too.
78
+
79
+
80
+ app = App(name="My App", description="Demo app.")
81
+ app.add_command_parser(Login(name="Login", description="Sign in."))
82
+ app.add_command_parser(Logout(name="Logout"), description="Sign out.")
83
+
84
+ print(app.get_arguments())
85
+ ```
86
+
87
+ ```console
88
+ $ python main.py login -u ana -p 123
89
+ {'command': 'login', 'username': 'ana', 'password': '123'}
90
+
91
+ $ python main.py logout --all
92
+ {'command': 'logout', 'all': True}
93
+
94
+ $ python main.py -h
95
+ usage: My App [-h] {login,logout} ...
96
+ ```
97
+
98
+ The chosen command is available under the `command` key.
99
+
100
+ ## Reusing arguments
101
+
102
+ Arguments from other parsers can be inherited with `parents`:
103
+
104
+ ```python
105
+ class Common(PyArg):
106
+ def setup_arguments(self) -> None:
107
+ self.add_argument("--verbose", "-v", action="store_true")
108
+
109
+
110
+ class Build(PyArg):
111
+ def setup_arguments(self) -> None:
112
+ self.add_argument("--target")
113
+
114
+
115
+ build = Build(name="Build", parents=[Common(name="Common", add_help=False)])
116
+ ```
117
+
118
+ Use `add_help=False` on the parent parser so the `-h` option is not registered twice.
119
+
120
+ ## API
121
+
122
+ ### `PyArg(name, description=None, parents=[], **kwargs)`
123
+
124
+ Abstract class. It cannot be instantiated directly, you must inherit from it and implement `setup_arguments`.
125
+
126
+ - `name`: parser name. Used as `prog` in the help text and to build the command name.
127
+ - `description`: text shown in `-h`.
128
+ - `parents`: other parsers whose arguments are copied into this one.
129
+ - `**kwargs`: forwarded to `argparse.ArgumentParser`. The `prog`, `description`, `parents` and `conflict_handler`
130
+ keys are ignored, as `conflict_handler` is always `"resolve"`.
131
+
132
+ ### `setup_arguments(self) -> None`
133
+
134
+ Abstract method. Define all the arguments of the parser here. It runs at the end of `__init__`.
135
+
136
+ ### `command_name`
137
+
138
+ Read-only property with the command name derived from `name`.
139
+
140
+ ### `add_command_parser(parser, description=None, script_entrypoint=None)`
141
+
142
+ Adds `parser` as a sub-command. If `description` is not set, the parser's own description is used.
143
+
144
+ `script_entrypoint` stores a callable on the command, but `get_arguments` does not call it and removes it from the
145
+ result.
146
+
147
+ ### `get_arguments() -> dict[str, Any]`
148
+
149
+ Parses `sys.argv` and returns the arguments passed by the user as a dictionary.
150
+
151
+ ## Development
152
+
153
+ See [DEVELOPERS.md](DEVELOPERS.md).
@@ -0,0 +1,6 @@
1
+ pyarg/__init__.py,sha256=R5_NfeiJii9p9hunfiYdfcHHXBwk2Dx7MN6K7caYlh8,101
2
+ pyarg/parser.py,sha256=vdtXC5DSymuLuuXUMXKy1S-YMIy-2N3ygdaGX1lMPBA,4175
3
+ py_arguments-1.0.1.dist-info/METADATA,sha256=G3pl3-Ns6cnqQABHR2maN_CQ3Z9_-Nm_eQArh1FfY9w,4141
4
+ py_arguments-1.0.1.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
5
+ py_arguments-1.0.1.dist-info/top_level.txt,sha256=y1U_g5JsWcbFiM6lo3f2O9pIKHiJOErDQLFdHIr4tcs,6
6
+ py_arguments-1.0.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ pyarg
pyarg/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """Entrypoint module file for pyarg library."""
2
+
3
+ from pyarg.parser import PyArg
4
+
5
+ __all__ = ["PyArg"]
pyarg/parser.py ADDED
@@ -0,0 +1,118 @@
1
+ import argparse
2
+ import re
3
+ from abc import ABC, abstractmethod
4
+ from collections.abc import Callable, Iterable
5
+ from typing import Any
6
+
7
+
8
+ # Classes.
9
+ class PyArg(ABC, argparse.ArgumentParser):
10
+ """
11
+ **PyArg** is an abstract class that uses `argparse.ArgumentParser` class that allows you to set up easily sub
12
+ parsers/commands from a parent parser.
13
+
14
+ Creates a class inhenrance from **PyArg**, set up all arguments in `setup_arguments` method, and then, you can
15
+ re-use the same parser definition to build your complex structure of parser arguments and CLI commands.
16
+
17
+ Usage:
18
+ ```
19
+ class Forms(PyArg):
20
+ def setup_arguments(self):
21
+ self.add_argument("--username", "-u")
22
+ self.add_argument("--password", "-p")
23
+
24
+ parser = Forms(name="Forms")
25
+ arguments = parser.get_arguments()
26
+ ```
27
+
28
+ Extra commands that trigger a function:
29
+ ```
30
+ class Application(PyArg):
31
+ def setup_arguments(self):
32
+ ...
33
+
34
+ class MakeDir(PyArg):
35
+ @staticmethod
36
+ def create_directory(location, name_list):
37
+ for name in name_list:
38
+ os.makedirs(os.path.join(location, name))
39
+
40
+ def setup_arguments(self):
41
+ self.add_argument("--location", "-l")
42
+ self.add_argument("--name", "-n", type=str, nargs="+")
43
+
44
+ class DeleteDir(PyArg):
45
+ @staticmethod
46
+ def delete_directory(path):
47
+ shutil.rmtree(path)
48
+
49
+ def setup_arguments(self):
50
+ self.add_argument("--path", "-p")
51
+
52
+ make_dir_parser = MakeDir(name="Make Directory")
53
+ delete_dir_parser = DeleteDir(name="Delete Directory")
54
+
55
+ app = Application(name="Directory Manager")
56
+ app.add_command_parser(parser=make_dir_parser, script_entrypoint=MakeDir.create_directory)
57
+ app.add_command_parser(parser=delete_dir_parser, script_entrypoint=DeleteDir.delete_directory)
58
+ app.get_arguments()
59
+ ```
60
+ """
61
+
62
+ # Special methods.
63
+ def __init__(
64
+ self, name: str, description: str | None = None, parents: Iterable["PyArg"] | None = None, **kwargs: Any
65
+ ) -> None:
66
+ # Remove parameters.
67
+ for to_remove in ("prog", "description", "parents", "conflict_handler"):
68
+ kwargs.pop(to_remove, None)
69
+ parents = [] if parents is None else parents
70
+ super().__init__(prog=name, description=description, parents=parents, conflict_handler="resolve", **kwargs)
71
+
72
+ # Attributes.
73
+ self.__command_name: str = self.__get_program_name(name=name)
74
+
75
+ # Set up extra sub parsers.
76
+ self.__subparser: None | argparse._SubParsersAction[argparse.ArgumentParser] = None
77
+
78
+ # Set up arguments.
79
+ self.setup_arguments()
80
+
81
+ # Properties.
82
+ @property
83
+ def command_name(self) -> str:
84
+ return self.__command_name
85
+
86
+ # Abstract methods.
87
+ @abstractmethod
88
+ def setup_arguments(self) -> None:
89
+ """Main method used to define all parameters of the parser."""
90
+
91
+ # Private methods.
92
+ def __get_program_name(self, name: str) -> str:
93
+ """Convert the parser name to a CLI command name."""
94
+ name = name.lower()
95
+ name = re.sub(pattern=r"\s+", repl="-", string=name.strip())
96
+ return name
97
+
98
+ # Methods.
99
+ def add_command_parser(
100
+ self, parser: "PyArg", description: str | None = None, script_entrypoint: Callable[..., Any] | None = None
101
+ ) -> None:
102
+ """Add an extra parser to a parent **PyArg** class to be called by `name`."""
103
+ if self.__subparser is None:
104
+ self.__subparser = self.add_subparsers(
105
+ dest="command", parser_class=argparse.ArgumentParser, help="Available commands."
106
+ )
107
+ subparser: argparse.ArgumentParser = self.__subparser.add_parser(
108
+ parents=[parser], name=parser.command_name, description=description or parser.description, add_help=False
109
+ )
110
+ # Add script.
111
+ if script_entrypoint:
112
+ subparser.set_defaults(_entrypoint=script_entrypoint)
113
+
114
+ def get_arguments(self) -> dict[str, Any]:
115
+ """Get the CLI arguments."""
116
+ args: dict[str, Any] = vars(self.parse_args())
117
+ args.pop("_entrypoint", None)
118
+ return args