simplifiedcli 1.0.0__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.
examples/basic.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Basic example."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from simpcli import Manager
|
|
8
|
+
from simpcli import get_logger
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
11
|
+
from logging import Logger
|
|
12
|
+
|
|
13
|
+
from simpcli import Result
|
|
14
|
+
|
|
15
|
+
__version__: str = "0.0.1"
|
|
16
|
+
|
|
17
|
+
logger: Logger = get_logger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
manager: Manager = Manager(prog=__name__, version=__version__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@manager.command()
|
|
24
|
+
def no_args() -> Result:
|
|
25
|
+
"""Command with no arguments."""
|
|
26
|
+
logger.info("no_args")
|
|
27
|
+
return 0
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@manager.command()
|
|
31
|
+
@manager.parameter("one", type=str)
|
|
32
|
+
@manager.parameter("two", type=int)
|
|
33
|
+
def positional_args(one: str, two: int) -> Result:
|
|
34
|
+
"""Command with positional arguments."""
|
|
35
|
+
logger.info("positional_args %s %s", one, two)
|
|
36
|
+
return 0
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@manager.command()
|
|
40
|
+
@manager.parameter("param", type=str)
|
|
41
|
+
@manager.parameter("-f", "--flag", action="store_true", type=bool)
|
|
42
|
+
def optional_args(param: str, flag: bool = False) -> Result: # noqa: FBT001, FBT002
|
|
43
|
+
"""Command with optional arguments."""
|
|
44
|
+
logger.info("flags %s %s", param, flag)
|
|
45
|
+
return 0
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
if __name__ == "__main__":
|
|
49
|
+
manager.handle_main()
|
simpcli/__init__.py
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
"""A simplified command line interface using argparse."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import sys
|
|
7
|
+
from argparse import ArgumentError
|
|
8
|
+
from argparse import ArgumentParser
|
|
9
|
+
from collections import deque
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from logging.handlers import TimedRotatingFileHandler
|
|
12
|
+
from typing import TYPE_CHECKING
|
|
13
|
+
from typing import NoReturn
|
|
14
|
+
from typing import Protocol
|
|
15
|
+
from typing import override
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
18
|
+
# noinspection PyProtectedMember
|
|
19
|
+
from argparse import _SubParsersAction
|
|
20
|
+
from collections.abc import Callable
|
|
21
|
+
from collections.abc import Mapping
|
|
22
|
+
from collections.abc import Sequence
|
|
23
|
+
from logging import FileHandler
|
|
24
|
+
from logging import Formatter
|
|
25
|
+
from logging import Handler
|
|
26
|
+
from logging import Logger
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
from typing import Any
|
|
29
|
+
from typing import Self
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
__all__: list[str] = [
|
|
33
|
+
"ARGUMENT_ERROR",
|
|
34
|
+
"NO_COMMAND_ERROR",
|
|
35
|
+
"NO_DEFAULT",
|
|
36
|
+
"Command",
|
|
37
|
+
"Manager",
|
|
38
|
+
"NoCommandError",
|
|
39
|
+
"NoDefault",
|
|
40
|
+
"Parameter",
|
|
41
|
+
"Result",
|
|
42
|
+
"get_logger",
|
|
43
|
+
"logger",
|
|
44
|
+
"logger_formatter",
|
|
45
|
+
"logger_handler",
|
|
46
|
+
"set_logger_file",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
type Result = int | str
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class CommandFunc(Protocol):
|
|
53
|
+
__name__: str
|
|
54
|
+
parameters: Sequence[Parameter]
|
|
55
|
+
|
|
56
|
+
def __call__(self, *args: Any, **kwargs: Any) -> Result: # pragma: no cover # noqa: ANN401
|
|
57
|
+
pass
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
logger_formatter: Formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")
|
|
61
|
+
|
|
62
|
+
logger_handler: Handler = logging.StreamHandler(sys.stdout)
|
|
63
|
+
logger_handler.setFormatter(logger_formatter)
|
|
64
|
+
logger_handler.setLevel(logging.INFO)
|
|
65
|
+
|
|
66
|
+
logger: Logger = logging.getLogger(__name__)
|
|
67
|
+
logger.propagate = False
|
|
68
|
+
logger.setLevel(logging.DEBUG)
|
|
69
|
+
logger.handlers = [logger_handler]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class NoDefault:
|
|
73
|
+
"""Singleton to declare that a parameter has no default values."""
|
|
74
|
+
|
|
75
|
+
__instance__: NoDefault | None = None
|
|
76
|
+
|
|
77
|
+
def __new__(cls) -> Self:
|
|
78
|
+
"""Create the singleton instance or return it."""
|
|
79
|
+
if cls.__instance__ is None:
|
|
80
|
+
cls.__instance__ = super().__new__(cls)
|
|
81
|
+
return cls.__instance__
|
|
82
|
+
|
|
83
|
+
@override
|
|
84
|
+
def __repr__(self) -> str:
|
|
85
|
+
return "NoDefault"
|
|
86
|
+
|
|
87
|
+
@classmethod
|
|
88
|
+
def remove_defaults(cls, **kwargs: Any) -> dict[str, Any]: # noqa: ANN401
|
|
89
|
+
"""Remove NoDefault from kwargs."""
|
|
90
|
+
no_default: NoDefault = NoDefault()
|
|
91
|
+
return {k: v for k, v in kwargs.items() if v is not no_default}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
NO_DEFAULT: NoDefault = NoDefault()
|
|
95
|
+
ARGUMENT_ERROR: int = 10
|
|
96
|
+
NO_COMMAND_ERROR: int = 11
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def get_logger(name: str) -> Logger:
|
|
100
|
+
"""Get a logger whose parent is simpcli.logger.
|
|
101
|
+
|
|
102
|
+
:param name: The name of the logger, usually __name__.
|
|
103
|
+
:return: The logger.
|
|
104
|
+
"""
|
|
105
|
+
if name == __name__:
|
|
106
|
+
return logger
|
|
107
|
+
|
|
108
|
+
new_logger: Logger = logging.getLogger(name)
|
|
109
|
+
new_logger.parent = logger
|
|
110
|
+
return new_logger
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def set_logger_file(file: Path | None = None, /, *, handler: FileHandler | None = None) -> None:
|
|
114
|
+
"""Set the log FileHandler at the file provided or to the FileHandler provided."""
|
|
115
|
+
file_handler: FileHandler
|
|
116
|
+
if file is None:
|
|
117
|
+
if handler is None:
|
|
118
|
+
message: str = "Must supply either 'file' or 'handler'"
|
|
119
|
+
raise ValueError(message)
|
|
120
|
+
file_handler = handler
|
|
121
|
+
elif handler is None:
|
|
122
|
+
file.parent.mkdir(parents=True, exist_ok=True)
|
|
123
|
+
file_handler = TimedRotatingFileHandler(file, when="D", backupCount=7)
|
|
124
|
+
file_handler.setLevel(logging.INFO)
|
|
125
|
+
file_handler.setFormatter(logger_formatter)
|
|
126
|
+
else:
|
|
127
|
+
message: str = "'file' and 'handler' are mutually exclusive"
|
|
128
|
+
raise ValueError(message)
|
|
129
|
+
|
|
130
|
+
logger.handlers = [logger_handler, file_handler]
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class NoCommandError(Exception):
|
|
134
|
+
"""Exception raised when no command is provided."""
|
|
135
|
+
|
|
136
|
+
def __init__(self, message: str = "No command provided") -> None: # noqa: D107
|
|
137
|
+
super().__init__(message)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@dataclass(frozen=True)
|
|
141
|
+
class Command:
|
|
142
|
+
"""A runnable command."""
|
|
143
|
+
|
|
144
|
+
kwargs: Mapping[str, Any]
|
|
145
|
+
parameters: Sequence[Parameter]
|
|
146
|
+
func: CommandFunc
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@dataclass(frozen=True)
|
|
150
|
+
class Parameter:
|
|
151
|
+
"""A parameter for a command."""
|
|
152
|
+
|
|
153
|
+
args: Sequence[str]
|
|
154
|
+
kwargs: Mapping[str, Any]
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@dataclass(init=False, frozen=True, kw_only=True)
|
|
158
|
+
class Manager:
|
|
159
|
+
"""Manages the command line interface."""
|
|
160
|
+
|
|
161
|
+
prog: str
|
|
162
|
+
version: str | None
|
|
163
|
+
kwargs: Mapping[str, Any]
|
|
164
|
+
|
|
165
|
+
global_parameters: list[Parameter]
|
|
166
|
+
commands: dict[str, Command]
|
|
167
|
+
|
|
168
|
+
def __init__(self, prog: str, version: str | None = None, **kwargs: Any) -> None: # noqa: ANN401
|
|
169
|
+
"""Initialize the manager."""
|
|
170
|
+
object.__setattr__(self, "prog", prog)
|
|
171
|
+
object.__setattr__(self, "version", version)
|
|
172
|
+
object.__setattr__(self, "kwargs", kwargs)
|
|
173
|
+
|
|
174
|
+
object.__setattr__(self, "global_parameters", [])
|
|
175
|
+
object.__setattr__(self, "commands", {})
|
|
176
|
+
|
|
177
|
+
def global_parameter(self, name_or_flag: str, *name_or_flags: str, **kwargs: Any) -> None: # noqa: ANN401
|
|
178
|
+
"""Add a global parameter to the manager."""
|
|
179
|
+
args: list[str] = [name_or_flag, *name_or_flags]
|
|
180
|
+
|
|
181
|
+
self.global_parameters.append(Parameter(args, kwargs))
|
|
182
|
+
|
|
183
|
+
def command[C: CommandFunc](self, **kwargs: Any) -> Callable[[C], C]: # noqa: ANN401
|
|
184
|
+
"""Designate the function as a command."""
|
|
185
|
+
|
|
186
|
+
def decorator(func: C) -> C:
|
|
187
|
+
# TODO(Ryan): inspect.signature(func, annotation_format=Format.FORWARD_REF)
|
|
188
|
+
parameters: Sequence[Parameter] = []
|
|
189
|
+
if hasattr(func, "parameters"):
|
|
190
|
+
parameters: Sequence[Parameter] = list(func.parameters)
|
|
191
|
+
delattr(func, "parameters")
|
|
192
|
+
self.commands[func.__name__] = Command(func=func, kwargs=kwargs, parameters=parameters)
|
|
193
|
+
|
|
194
|
+
return func
|
|
195
|
+
|
|
196
|
+
return decorator
|
|
197
|
+
|
|
198
|
+
@staticmethod
|
|
199
|
+
def parameter[C: CommandFunc](
|
|
200
|
+
name_or_flag: str,
|
|
201
|
+
*name_or_flags: str,
|
|
202
|
+
**kwargs: Any, # noqa: ANN401
|
|
203
|
+
) -> Callable[[C], C]:
|
|
204
|
+
"""Add additional configuration to a command parameter."""
|
|
205
|
+
args: list[str] = [name_or_flag, *name_or_flags]
|
|
206
|
+
|
|
207
|
+
def decorator(func: C) -> C:
|
|
208
|
+
parameters: deque[Parameter] = getattr(func, "parameters", deque())
|
|
209
|
+
parameters.appendleft(Parameter(args, kwargs))
|
|
210
|
+
func.parameters = parameters
|
|
211
|
+
return func
|
|
212
|
+
|
|
213
|
+
return decorator
|
|
214
|
+
|
|
215
|
+
def run(self, *args: Any) -> Result: # noqa: ANN401
|
|
216
|
+
"""Run the command line interface.
|
|
217
|
+
|
|
218
|
+
:param args: The raw arguments to pass to the command.
|
|
219
|
+
:return: The result of the command.
|
|
220
|
+
"""
|
|
221
|
+
result: Result
|
|
222
|
+
try:
|
|
223
|
+
parser: ArgumentParser = self.create_parser()
|
|
224
|
+
|
|
225
|
+
cleaned_args: list[str] = list(map(str, args))
|
|
226
|
+
parsed_args: dict[str, Any] = dict(vars(parser.parse_args(cleaned_args)))
|
|
227
|
+
|
|
228
|
+
result = self.__handle_command(parsed_args)
|
|
229
|
+
except ArgumentError as e:
|
|
230
|
+
# Raised when ArgumentParser fails to parse the arguments.
|
|
231
|
+
logger.exception("Argparse error:", exc_info=e)
|
|
232
|
+
result = ARGUMENT_ERROR
|
|
233
|
+
except NoCommandError as e:
|
|
234
|
+
# Raised when no command is provided.
|
|
235
|
+
logger.exception("No command error:", exc_info=e)
|
|
236
|
+
result = NO_COMMAND_ERROR
|
|
237
|
+
except BaseException as e:
|
|
238
|
+
logger.exception("Unhandled Exception:", exc_info=e)
|
|
239
|
+
result = -1
|
|
240
|
+
|
|
241
|
+
return result
|
|
242
|
+
|
|
243
|
+
def handle_main(self) -> NoReturn:
|
|
244
|
+
"""Handle main."""
|
|
245
|
+
args: list[str] = sys.argv[1:]
|
|
246
|
+
result: Result = self.run(*args)
|
|
247
|
+
sys.exit(result)
|
|
248
|
+
|
|
249
|
+
def create_parser(self) -> ArgumentParser:
|
|
250
|
+
"""Create the ArgumentParser instance."""
|
|
251
|
+
prog: str = f"{self.prog}.exe" if getattr(sys, "frozen", False) else f"{self.prog}.py"
|
|
252
|
+
kwargs: dict[str, Any] = dict(self.kwargs)
|
|
253
|
+
kwargs.setdefault("exit_on_error", False)
|
|
254
|
+
parser: ArgumentParser = ArgumentParser(prog=prog, **kwargs)
|
|
255
|
+
|
|
256
|
+
parser.add_argument("--verbose", action="store_true")
|
|
257
|
+
if self.version is not None:
|
|
258
|
+
parser.add_argument("-v", "--version", action="version", version=self.version)
|
|
259
|
+
|
|
260
|
+
parameter: Parameter
|
|
261
|
+
for parameter in self.global_parameters:
|
|
262
|
+
parser.add_argument(*parameter.args, **parameter.kwargs)
|
|
263
|
+
|
|
264
|
+
# User Defined Commands
|
|
265
|
+
command_parser: _SubParsersAction = parser.add_subparsers(
|
|
266
|
+
dest="command",
|
|
267
|
+
metavar="command",
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
command_name: str | None
|
|
271
|
+
command: Command
|
|
272
|
+
for command_name, command in self.commands.items():
|
|
273
|
+
command_kwargs: dict[str, Any] = dict(command.kwargs)
|
|
274
|
+
command_kwargs.setdefault("description", command.func.__doc__)
|
|
275
|
+
|
|
276
|
+
user_command_parser: ArgumentParser = command_parser.add_parser(
|
|
277
|
+
command_name,
|
|
278
|
+
**command_kwargs,
|
|
279
|
+
)
|
|
280
|
+
parameter: Parameter
|
|
281
|
+
for parameter in command.parameters:
|
|
282
|
+
user_command_parser.add_argument(*parameter.args, **parameter.kwargs)
|
|
283
|
+
|
|
284
|
+
return parser
|
|
285
|
+
|
|
286
|
+
def __handle_command(self, parsed_args: dict[str, Any]) -> Result:
|
|
287
|
+
if parsed_args.pop("verbose", False):
|
|
288
|
+
logger_handler.setLevel(logging.DEBUG)
|
|
289
|
+
|
|
290
|
+
command_name: str = parsed_args.pop("command")
|
|
291
|
+
command: Command | None = self.commands.get(command_name, None)
|
|
292
|
+
if command is None:
|
|
293
|
+
raise NoCommandError
|
|
294
|
+
|
|
295
|
+
return command.func(**parsed_args)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: simplifiedcli
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: A simplified command line interface.
|
|
5
|
+
Requires-Python: >=3.13
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Provides-Extra: dev
|
|
8
|
+
Requires-Dist: tox>=4.33.0; extra == "dev"
|
|
9
|
+
Requires-Dist: pytest>=8.4.2; extra == "dev"
|
|
10
|
+
|
|
11
|
+
# SimplifiedCLI
|
|
12
|
+
|
|
13
|
+
**A simple CLI for python applications.**
|
|
14
|
+
|
|
15
|
+
## Basic Example:
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
"""Basic example."""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from typing import TYPE_CHECKING
|
|
23
|
+
|
|
24
|
+
from simpcli import Manager
|
|
25
|
+
from simpcli import get_logger
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
28
|
+
from logging import Logger
|
|
29
|
+
|
|
30
|
+
from simpcli import Result
|
|
31
|
+
|
|
32
|
+
__version__: str = "0.0.1"
|
|
33
|
+
|
|
34
|
+
logger: Logger = get_logger(__name__)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
manager: Manager = Manager(prog=__name__, version=__version__)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@manager.command()
|
|
41
|
+
def no_args() -> Result:
|
|
42
|
+
"""Command with no arguments."""
|
|
43
|
+
logger.info("no_args")
|
|
44
|
+
return 0
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@manager.command()
|
|
48
|
+
@manager.parameter("one", type=str)
|
|
49
|
+
@manager.parameter("two", type=int)
|
|
50
|
+
def positional_args(one: str, two: int) -> Result:
|
|
51
|
+
"""Command with positional arguments."""
|
|
52
|
+
logger.info("positional_args %s %s", one, two)
|
|
53
|
+
return 0
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@manager.command()
|
|
57
|
+
@manager.parameter("param", type=str)
|
|
58
|
+
@manager.parameter("-f", "--flag", action="store_true", type=bool)
|
|
59
|
+
def optional_args(param: str, flag: bool = False) -> Result: # noqa: FBT001, FBT002
|
|
60
|
+
"""Command with optional arguments."""
|
|
61
|
+
logger.info("flags %s %s", param, flag)
|
|
62
|
+
return 0
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
if __name__ == "__main__":
|
|
66
|
+
manager.handle_main()
|
|
67
|
+
|
|
68
|
+
```
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
examples/basic.py,sha256=UU2OZ6g1uwev8wNICgC6RYhBtacxcrGAUPPfsFgfqrA,1176
|
|
2
|
+
simpcli/__init__.py,sha256=oqX9lYMJaWL6pIVXJXi-ceGzIRCEZXfuCi2Jn6ZfE9c,9739
|
|
3
|
+
simplifiedcli-1.0.0.dist-info/METADATA,sha256=s6vyjjJrfFUKpHcIOr0OviIPhhS4YPjxCEVdT0bIiDg,1565
|
|
4
|
+
simplifiedcli-1.0.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
5
|
+
simplifiedcli-1.0.0.dist-info/top_level.txt,sha256=MbY8yAJZ9oLOlmyowH0iT6Vjhgtf7WyOVl4PpjCmTQ0,17
|
|
6
|
+
simplifiedcli-1.0.0.dist-info/RECORD,,
|