xkits-command 0.1a1__tar.gz → 0.2__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: xkits-command
3
- Version: 0.1a1
3
+ Version: 0.2
4
4
  Summary: Command line module
5
5
  Home-page: https://github.com/bondbox/xcommand/
6
6
  Author: Mingzhe Zou
@@ -9,13 +9,15 @@ License: GPLv2
9
9
  Project-URL: Source Code, https://github.com/bondbox/xcommand/
10
10
  Project-URL: Bug Tracker, https://github.com/bondbox/xcommand/issues
11
11
  Project-URL: Documentation, https://github.com/bondbox/xcommand/
12
- Keywords: command-line,argparse,argcomplete
12
+ Keywords: command-line,argparse,argcomplete,shell,bash,terminal
13
13
  Platform: any
14
14
  Classifier: Programming Language :: Python
15
15
  Classifier: Programming Language :: Python :: 3
16
16
  Requires-Python: >=3.8
17
17
  Description-Content-Type: text/markdown
18
18
  License-File: LICENSE
19
+ Requires-Dist: argcomplete>=3.2.1
20
+ Requires-Dist: xkits_logger>=0.1
19
21
 
20
22
  # xcommand
21
23
 
@@ -1,5 +1,5 @@
1
1
  [metadata]
2
- keywords = command-line, argparse, argcomplete
2
+ keywords = command-line, argparse, argcomplete, shell, bash, terminal
3
3
  long_description = file: README.md
4
4
  long_description_content_type = text/markdown
5
5
  license = GPLv2
@@ -0,0 +1,9 @@
1
+ # coding:utf-8
2
+
3
+ from xkits_command.actuator import Command # noqa:F401
4
+ from xkits_command.actuator import CommandArgument # noqa:F401,H306
5
+ from xkits_command.actuator import CommandCreation # noqa:F401
6
+ from xkits_command.actuator import CommandDeletion # noqa:F401
7
+ from xkits_command.actuator import CommandExecutor # noqa:F401
8
+ from xkits_command.actuator import Namespace # noqa:F401
9
+ from xkits_command.parser import ArgParser # noqa:F401
@@ -0,0 +1,724 @@
1
+ # coding:utf-8
2
+
3
+ from argparse import Namespace
4
+ from errno import ECANCELED
5
+ from errno import EINVAL
6
+ from errno import ENOENT
7
+ from errno import ENOTRECOVERABLE
8
+ import logging
9
+ from logging import Logger
10
+ from os import getenv
11
+ import sys
12
+ from typing import Any
13
+ from typing import Callable
14
+ from typing import Dict
15
+ from typing import List
16
+ from typing import Optional
17
+ from typing import Sequence
18
+ from typing import Tuple
19
+
20
+ from xkits_logger.logger import Logger as Log
21
+
22
+ from xkits_command.attribute import __project__
23
+ from xkits_command.parser import ArgParser
24
+
25
+
26
+ class CommandArgument:
27
+ '''Define a new command-line node.
28
+
29
+ For example:
30
+
31
+ >>> from xkits_command import ArgParser\n
32
+ >>> from xkits_command import CommandArgument\n
33
+
34
+ >>> @CommandArgument("example")\n
35
+ >>> def cmd(_arg: ArgParser):\n
36
+ >>> _arg.add_opt_on("-t", "--test")\n
37
+ '''
38
+
39
+ def __init__(self, name: str, **kwargs):
40
+ '''Initialize the node.
41
+
42
+ @param name: Node name
43
+ @type name: str
44
+
45
+ @param description: Text to display before the argument help
46
+ @type description: str (by default, no text)
47
+
48
+ @param epilog: Text to display after the argument help
49
+ @type epilog: str (by default, no text)
50
+
51
+ @param help: Help message as a subcommand
52
+ @type help: str
53
+
54
+ @param add_help: Add a -h/--help option to the node
55
+ @type add_help: bool (default: True)
56
+ '''
57
+ if "help" in kwargs and "description" not in kwargs:
58
+ kwargs["description"] = kwargs["help"]
59
+ if "description" in kwargs and "help" not in kwargs:
60
+ kwargs["help"] = kwargs["description"]
61
+ self.__name: str = name
62
+ self.__prev: CommandArgument = self
63
+ self.__cmds: Command = Command()
64
+ self.__options: Dict[str, Any] = kwargs
65
+ self.__bind: Optional[CommandExecutor] = None
66
+ self.__subs: Optional[Tuple[CommandArgument, ...]] = None
67
+ self.__func: Optional[Callable[[ArgParser], None]] = None
68
+
69
+ def __call__(self, cmd_func: Callable[[ArgParser], None]):
70
+ self.__func = cmd_func
71
+ return self
72
+
73
+ @property
74
+ def func(self) -> Callable[[ArgParser], None]:
75
+ if self.__func is None:
76
+ raise ValueError("No function") # pragma: no cover
77
+ return self.__func
78
+
79
+ @property
80
+ def name(self) -> str:
81
+ return self.__name
82
+
83
+ @property
84
+ def root(self) -> "CommandArgument":
85
+ root = self.__prev
86
+ while root.prev != root:
87
+ root = root.prev
88
+ return root
89
+
90
+ @property
91
+ def prev(self) -> "CommandArgument":
92
+ return self.__prev
93
+
94
+ @prev.setter
95
+ def prev(self, value: "CommandArgument"):
96
+ assert isinstance(value, CommandArgument)
97
+ self.__prev = value
98
+
99
+ @property
100
+ def cmds(self) -> "Command":
101
+ return self.__cmds
102
+
103
+ @property
104
+ def options(self) -> Dict[str, Any]:
105
+ return self.__options
106
+
107
+ @property
108
+ def bind(self) -> Optional["CommandExecutor"]:
109
+ return self.__bind
110
+
111
+ @bind.setter
112
+ def bind(self, value: "CommandExecutor"):
113
+ assert isinstance(value, CommandExecutor)
114
+ self.__bind = value
115
+
116
+ @property
117
+ def subs(self) -> Optional[Tuple["CommandArgument", ...]]:
118
+ return self.__subs
119
+
120
+ @subs.setter
121
+ def subs(self, value: Tuple["CommandArgument", ...]):
122
+ assert isinstance(value, Tuple)
123
+ for sub in value:
124
+ assert isinstance(sub, CommandArgument)
125
+ self.__subs = value
126
+
127
+ @property
128
+ def sub_dest(self) -> str:
129
+ node: CommandArgument = self
130
+ subs: List[str] = [self.name]
131
+ while node.prev is not node:
132
+ node = node.prev
133
+ subs.insert(0, node.name)
134
+ name = "_".join(subs)
135
+ return f"__sub_dest_{name}__"
136
+
137
+
138
+ class CommandExecutor:
139
+ '''Define the main callback function, and bind it to a node and
140
+ all subcommands.
141
+
142
+ For example:
143
+
144
+ >>> from xkits_command import Command\n
145
+ >>> from xkits_command import CommandExecutor\n
146
+
147
+ >>> @CommandExecutor(cmd, cmd_get, cmd_set)\n
148
+ >>> def run(cmds: Command) -> int:\n
149
+ >>> return 0\n
150
+ '''
151
+
152
+ def __init__(self, cmd_bind: CommandArgument, *sub_cmds: CommandArgument,
153
+ skip: bool = False):
154
+ '''Initialize the node.
155
+
156
+ @param cmd_bind: Bind to a root command node
157
+ @type name: CommandArgument
158
+
159
+ @param *sub_cmds: All required subcommands
160
+ @type *sub_cmds: CommandArgument
161
+
162
+ @param skip: This node (CommandExecutor, CommandCreation and
163
+ CommandDeletion) does not run when a subcommand is specified,
164
+ run this node without any subcommands
165
+ @type skip: bool (default: False)
166
+ '''
167
+ assert isinstance(cmd_bind, CommandArgument)
168
+ assert isinstance(skip, bool)
169
+ cmd_bind.bind = self
170
+ cmd_bind.subs = sub_cmds
171
+ for sub in sub_cmds:
172
+ sub.prev = cmd_bind
173
+ cmd_bind.cmds.root = cmd_bind.root
174
+ self.__skip: bool = skip
175
+ self.__bind: CommandArgument = cmd_bind
176
+ self.__prep: Optional["CommandCreation"] = None
177
+ self.__done: Optional["CommandDeletion"] = None
178
+ self.__func: Optional[Callable[["Command"], int]] = None
179
+
180
+ def __call__(self, run_func: Callable[["Command"], int]):
181
+ self.__func = run_func
182
+ return self
183
+
184
+ @property
185
+ def func(self) -> Callable[["Command"], int]:
186
+ if self.__func is None:
187
+ raise ValueError("No function") # pragma: no cover
188
+ return self.__func
189
+
190
+ @property
191
+ def bind(self) -> CommandArgument:
192
+ return self.__bind
193
+
194
+ @property
195
+ def prep(self) -> Optional["CommandCreation"]:
196
+ return self.__prep
197
+
198
+ @prep.setter
199
+ def prep(self, value: "CommandCreation"):
200
+ assert isinstance(value, CommandCreation)
201
+ self.__prep = value
202
+
203
+ @property
204
+ def done(self) -> Optional["CommandDeletion"]:
205
+ return self.__done
206
+
207
+ @done.setter
208
+ def done(self, value: "CommandDeletion"):
209
+ assert isinstance(value, CommandDeletion)
210
+ self.__done = value
211
+
212
+ @property
213
+ def skip(self) -> bool:
214
+ return self.__skip
215
+
216
+
217
+ class CommandCreation:
218
+ '''Define prepare callback function, and bind it with main callback.
219
+
220
+ For example:
221
+
222
+ >>> from xkits_command import Command\n
223
+ >>> from xkits_command import CommandCreation\n
224
+ >>> from xkits_command import CommandExecutor\n
225
+
226
+ >>> @CommandExecutor(cmd, cmd_get, cmd_set)\n
227
+ >>> def run(cmds: Command) -> int:\n
228
+ >>> return 0\n
229
+
230
+ >>> @CommandCreation(run)\n
231
+ >>> def pre(cmds: Command) -> int:\n
232
+ >>> return 0\n
233
+ '''
234
+
235
+ def __init__(self, run_bind: CommandExecutor):
236
+ '''Initialize the node.
237
+
238
+ @param cmd_bind: Bind to a root command node
239
+ @type name: CommandArgument
240
+ '''
241
+ assert isinstance(run_bind, CommandExecutor)
242
+ run_bind.prep = self
243
+ self.__main: CommandExecutor = run_bind
244
+ self.__func: Optional[Callable[["Command"], int]] = None
245
+
246
+ def __call__(self, run_func: Callable[["Command"], int]):
247
+ self.__func = run_func
248
+ return self
249
+
250
+ @property
251
+ def func(self) -> Callable[["Command"], int]:
252
+ if self.__func is None:
253
+ raise ValueError("No function") # pragma: no cover
254
+ return self.__func
255
+
256
+ @property
257
+ def main(self) -> CommandExecutor:
258
+ return self.__main
259
+
260
+
261
+ class CommandDeletion:
262
+ '''Define purge callback function, and bind it with main callback.
263
+
264
+ For example:
265
+
266
+ >>> from xkits_command import Command\n
267
+ >>> from xkits_command import CommandDeletion\n
268
+ >>> from xkits_command import CommandExecutor\n
269
+
270
+ >>> @CommandExecutor(cmd, cmd_get, cmd_set)\n
271
+ >>> def run(cmds: Command) -> int:\n
272
+ >>> return 0\n
273
+
274
+ >>> @CommandDeletion(run)\n
275
+ >>> def end(cmds: Command) -> int:\n
276
+ >>> return 0\n
277
+ '''
278
+
279
+ def __init__(self, run_bind: CommandExecutor):
280
+ '''Initialize the node.
281
+
282
+ @param cmd_bind: Bind to a root command node
283
+ @type name: CommandArgument
284
+ '''
285
+ assert isinstance(run_bind, CommandExecutor)
286
+ run_bind.done = self
287
+ self.__main: CommandExecutor = run_bind
288
+ self.__func: Optional[Callable[["Command"], int]] = None
289
+
290
+ def __call__(self, run_func: Callable[["Command"], int]):
291
+ self.__func = run_func
292
+ return self
293
+
294
+ @property
295
+ def func(self) -> Callable[["Command"], int]:
296
+ if self.__func is None:
297
+ raise ValueError("No function") # pragma: no cover
298
+ return self.__func
299
+
300
+ @property
301
+ def main(self) -> CommandExecutor:
302
+ return self.__main
303
+
304
+
305
+ class Command(Log):
306
+ '''Singleton command-line tool based on argparse.
307
+
308
+ Define and bind all callback functions before calling run() or parse().
309
+
310
+ For example:
311
+
312
+ >>> from typing import Optional\n
313
+ >>> from typing import Sequence\n
314
+
315
+ >>> from xkits_command import ArgParser\n
316
+ >>> from xkits_command import Command\n
317
+ >>> from xkits_command import CommandArgument\n
318
+ >>> from xkits_command import CommandCreation\n
319
+ >>> from xkits_command import CommandDeletion\n
320
+ >>> from xkits_command import CommandExecutor\n
321
+
322
+ >>> @CommandArgument("example")\n
323
+ >>> def cmd(_arg: ArgParser):\n
324
+ >>> _arg.add_opt_on("-t", "--test")\n
325
+
326
+ >>> @CommandExecutor(cmd, cmd_get, cmd_set)\n
327
+ >>> def run(cmds: Command) -> int:\n
328
+ >>> return 0\n
329
+
330
+ >>> @CommandCreation(run)\n
331
+ >>> def pre(cmds: Command) -> int:\n
332
+ >>> return 0\n
333
+
334
+ >>> @CommandDeletion(run)\n
335
+ >>> def end(cmds: Command) -> int:\n
336
+ >>> return 0\n
337
+
338
+ >>> def main(argv: Optional[Sequence[str]] = None) -> int:\n
339
+ >>> return Command().run(\n
340
+ >>> root=cmd,\n
341
+ >>> argv=argv,\n
342
+ >>> prog="xkits-command-example",\n
343
+ >>> description="Simple command-line tool based on argparse.")\n
344
+ '''
345
+
346
+ LOGGER_ARGUMENT_GROUP = "logger options"
347
+
348
+ __INSTANCE: Optional["Command"] = None
349
+ __INITIALIZED: bool = False
350
+
351
+ def __init__(self):
352
+ if not self.__INITIALIZED:
353
+ self.__prog: str = __project__
354
+ self.__root: Optional[CommandArgument] = None
355
+ self.__args: Namespace = Namespace()
356
+ self.__version: Optional[str] = None
357
+ self.__enabled_logger: bool = True
358
+ self.__INITIALIZED = True
359
+ super().__init__()
360
+
361
+ def __new__(cls):
362
+ if not cls.__INSTANCE:
363
+ cls.__INSTANCE = super(Command, cls).__new__(cls)
364
+ return cls.__INSTANCE
365
+
366
+ @property
367
+ def prog(self) -> str:
368
+ return self.__prog
369
+
370
+ @property
371
+ def root(self) -> Optional[CommandArgument]:
372
+ '''Root Command.'''
373
+ return self.__root
374
+
375
+ @root.setter
376
+ def root(self, value: CommandArgument):
377
+ assert isinstance(value, CommandArgument)
378
+ self.__root = value
379
+
380
+ @property
381
+ def args(self) -> Namespace:
382
+ '''Namespace after parse arguments.'''
383
+ assert isinstance(self.__args, Namespace)
384
+ return self.__args
385
+
386
+ @args.setter
387
+ def args(self, value: Namespace):
388
+ assert isinstance(value, Namespace)
389
+ self.__args = value
390
+
391
+ @property
392
+ def version(self) -> Optional[str]:
393
+ '''Custom version for "-v" or "--version" output.'''
394
+ return self.__version
395
+
396
+ @version.setter
397
+ def version(self, value: str):
398
+ assert isinstance(value, str)
399
+ _version = value.strip()
400
+ self.__version = _version
401
+
402
+ @property
403
+ def enabled_logger(self) -> bool:
404
+ return self.__enabled_logger
405
+
406
+ @enabled_logger.setter
407
+ def enabled_logger(self, value: bool):
408
+ assert isinstance(value, bool)
409
+ self.__enabled_logger = value
410
+
411
+ @property
412
+ def logger(self) -> Logger:
413
+ '''Logger.'''
414
+ return self.get_logger(self.prog)
415
+
416
+ def __add_optional_version(self, _arg: ArgParser):
417
+ version = self.version
418
+ if not isinstance(version, str):
419
+ return
420
+
421
+ options = _arg.filter_optional_name("-v", "--version")
422
+ if len(options) > 0:
423
+ _arg.add_argument(*options, action="version",
424
+ version=f"%(prog)s {version.strip()}")
425
+
426
+ def __add_inner_parser_tail(self, _arg: ArgParser):
427
+
428
+ def filter_optional_name(*name: str) -> Optional[str]:
429
+ options = _arg.filter_optional_name(*name)
430
+ if len(options) > 0:
431
+ for i in name:
432
+ if i in options:
433
+ return i
434
+ return None
435
+
436
+ def add_optional_level():
437
+ group = _arg.argument_group(self.LOGGER_ARGUMENT_GROUP)
438
+ group_level = group.add_mutually_exclusive_group()
439
+
440
+ option_level = filter_optional_name("--level", "--log-level")
441
+ if isinstance(option_level, str):
442
+ DEF_LOG_LEVEL: str = getenv("LOG_LEVEL", self.LOG_LEVELS.INFO.value).lower() # noqa:E501
443
+ group_level.add_argument(
444
+ option_level,
445
+ type=str,
446
+ nargs="?",
447
+ const=DEF_LOG_LEVEL,
448
+ default=DEF_LOG_LEVEL,
449
+ choices=self.ALLOWED_LOG_LEVELS,
450
+ dest="_log_level_str_",
451
+ help=f"Logger output level, default is {DEF_LOG_LEVEL}.")
452
+
453
+ for level in self.ALLOWED_LOG_LEVELS:
454
+ options = []
455
+ if isinstance(filter_optional_name(f"-{level[0]}"), str):
456
+ options.append(f"-{level[0]}")
457
+ if isinstance(filter_optional_name(f"--{level}"), str):
458
+ options.append(f"--{level}")
459
+ elif isinstance(filter_optional_name(f"--{level}-level"), str):
460
+ options.append(f"--{level}-level")
461
+
462
+ if not options:
463
+ continue
464
+ group_level.add_argument(*options,
465
+ action="store_const",
466
+ const=level,
467
+ dest="_log_level_str_",
468
+ help=f"Logger level set to {level}.")
469
+
470
+ def add_optional_stream():
471
+ option = filter_optional_name("--log", "--log-file")
472
+ if not isinstance(option, str):
473
+ return
474
+
475
+ group = _arg.argument_group(self.LOGGER_ARGUMENT_GROUP)
476
+ group.add_argument(option,
477
+ type=str,
478
+ nargs=1,
479
+ default=[],
480
+ metavar="FILE",
481
+ action="extend",
482
+ dest="_log_files_",
483
+ help="Logger output to file.")
484
+
485
+ def add_optional_format():
486
+ option = filter_optional_name("--format", "--log-format")
487
+ if not isinstance(option, str):
488
+ return
489
+
490
+ DEFAULT_LOG_FMT = "%(log_color)s%(asctime)s"\
491
+ " %(process)d %(threadName)s %(levelname)s"\
492
+ " %(funcName)s %(filename)s:%(lineno)s"\
493
+ " %(message)s"
494
+
495
+ group = _arg.argument_group(self.LOGGER_ARGUMENT_GROUP)
496
+ group.add_argument(option,
497
+ type=str,
498
+ nargs="?",
499
+ const=DEFAULT_LOG_FMT,
500
+ default=self.DEFAULT_LOG_FORMAT,
501
+ metavar="STRING",
502
+ dest="_log_format_",
503
+ help="Logger output format.")
504
+
505
+ def add_optional_console():
506
+ group = _arg.argument_group(self.LOGGER_ARGUMENT_GROUP)
507
+ group_std = group.add_mutually_exclusive_group()
508
+
509
+ option = filter_optional_name("--stdout", "--log-stdout")
510
+ if isinstance(option, str):
511
+ group_std.add_argument(option,
512
+ const=sys.stdout,
513
+ action="store_const",
514
+ dest="_log_console_",
515
+ help="Logger output to stdout.")
516
+
517
+ option = filter_optional_name("--stderr", "--log-stderr")
518
+ if isinstance(option, str):
519
+ group_std.add_argument(option,
520
+ const=sys.stderr,
521
+ action="store_const",
522
+ dest="_log_console_",
523
+ help="Logger output to stderr.")
524
+
525
+ if self.enabled_logger:
526
+ add_optional_level()
527
+ add_optional_stream()
528
+ add_optional_format()
529
+ add_optional_console()
530
+
531
+ def __parse_logger(self, args: Namespace):
532
+ if not self.enabled_logger:
533
+ return
534
+
535
+ def parse_format() -> Optional[str]:
536
+ if hasattr(args, "_log_format_"):
537
+ fmt = getattr(args, "_log_format_")
538
+ if isinstance(fmt, str):
539
+ return fmt
540
+ return None
541
+
542
+ def parse_level() -> Optional[str]:
543
+ if hasattr(args, "_log_level_str_"):
544
+ level = getattr(args, "_log_level_str_")
545
+ if isinstance(level, str):
546
+ return level.upper()
547
+ return None
548
+
549
+ def parse_console() -> Optional[Any]:
550
+ return getattr(args, "_log_console_", None)
551
+
552
+ def parse_files() -> List[str]:
553
+ return getattr(args, "_log_files_", [])
554
+
555
+ fmt: Optional[str] = parse_format()
556
+ level_name: Optional[str] = parse_level()
557
+ console: Optional[Any] = parse_console()
558
+
559
+ handlers: List[logging.Handler] = []
560
+ if console is not None:
561
+ handlers.append(Log.new_stream_handler(stream=console, fmt=fmt))
562
+ for filename in parse_files():
563
+ handlers.append(Log.new_file_handler(filename=filename, fmt=fmt))
564
+ self.initiate_logger(self.logger, level=level_name, handlers=handlers)
565
+
566
+ def __add_parser(self, _map: Dict[CommandArgument, ArgParser],
567
+ arg_root: ArgParser, cmd_root: CommandArgument, **kwargs):
568
+ assert isinstance(cmd_root, CommandArgument)
569
+ assert cmd_root not in _map
570
+ _map[cmd_root] = arg_root
571
+
572
+ if not cmd_root.subs or len(cmd_root.subs) <= 0:
573
+ return
574
+
575
+ _sub = arg_root.add_subparsers(dest=cmd_root.sub_dest)
576
+ for sub in cmd_root.subs:
577
+ assert isinstance(sub, CommandArgument)
578
+ options = sub.options.copy()
579
+ for key, value in kwargs.items():
580
+ options.setdefault(key, value)
581
+ options.setdefault("epilog", arg_root.epilog)
582
+ options.setdefault("prev_parser", arg_root)
583
+ _arg: ArgParser = _sub.add_parser(sub.name, **options)
584
+ self.__add_parser(_map, _arg, sub)
585
+
586
+ def __add_option(self, _map: Dict[CommandArgument, ArgParser]):
587
+ for _cmd, _arg in _map.items():
588
+ _cmd.func(_arg)
589
+ self.__add_inner_parser_tail(_arg)
590
+
591
+ @classmethod
592
+ def check_error(cls, value: Any) -> int:
593
+ '''Check value is an error.
594
+
595
+ Return True if value is an error, otherwise False.
596
+ '''
597
+ return value if isinstance(value, int) else 0 if value in (None, True) else EINVAL # noqa:E501
598
+
599
+ def parse(self, root: Optional[CommandArgument] = None,
600
+ argv: Optional[Sequence[str]] = None, **kwargs) -> Namespace:
601
+ '''Parse the command line.'''
602
+ if root is None:
603
+ root = self.root
604
+ assert isinstance(root, CommandArgument)
605
+
606
+ _map: Dict[CommandArgument, ArgParser] = {}
607
+ _arg = ArgParser(argv=argv, **kwargs)
608
+ self.__prog = _arg.prog
609
+ self.__add_optional_version(_arg)
610
+ # To support preparse_from_sys_argv(), all subparsers must be added
611
+ # first. Otherwise, an error will occur during the help action.
612
+ self.__add_parser(_map, _arg, root, **kwargs)
613
+ self.__add_option(_map)
614
+
615
+ args = _arg.parse_args(args=argv)
616
+ assert isinstance(args, Namespace)
617
+ self.__parse_logger(args)
618
+ self.args = args
619
+ return self.args
620
+
621
+ def has_sub(self, root: CommandArgument,
622
+ args: Optional[Namespace] = None) -> bool:
623
+ '''If the root command node has any subcommand nodes, return true.
624
+
625
+ @param root: Command node
626
+ @type root: CommandArgument
627
+
628
+ @param args: Command arguments
629
+ @type args: Namespace or None (default self.args if None is specified)
630
+
631
+ @return: bool
632
+ '''
633
+ if args is None:
634
+ args = self.args
635
+ assert isinstance(root, CommandArgument)
636
+ assert isinstance(args, Namespace)
637
+ return isinstance(getattr(args, root.sub_dest), str)\
638
+ if hasattr(args, root.sub_dest) else False
639
+
640
+ def __run(self, args: Namespace, root: CommandArgument) -> int:
641
+ assert isinstance(root, CommandArgument)
642
+ assert isinstance(root.bind, CommandExecutor)
643
+
644
+ if not root.bind.skip or not self.has_sub(root, args):
645
+ ret = root.bind.func(self)
646
+ if self.check_error(ret):
647
+ return ret
648
+
649
+ if hasattr(args, root.sub_dest):
650
+ sub_dest = getattr(args, root.sub_dest)
651
+ if isinstance(sub_dest, str):
652
+ assert isinstance(root.subs, (list, tuple))
653
+ for sub in root.subs:
654
+ assert isinstance(sub, CommandArgument)
655
+ if sub.name == sub_dest:
656
+ ret = self.__run(args, sub)
657
+ if self.check_error(ret):
658
+ return ret
659
+
660
+ done = root.bind.done
661
+ if done is not None:
662
+ assert isinstance(done, CommandDeletion)
663
+ if not root.bind.skip or not self.has_sub(root, args):
664
+ ret = done.func(self) # purge
665
+ if self.check_error(ret):
666
+ return ret
667
+ return 0
668
+
669
+ def __pre(self, args: Namespace, root: CommandArgument) -> int:
670
+ assert isinstance(root, CommandArgument)
671
+ assert isinstance(root.bind, CommandExecutor)
672
+
673
+ prep = root.bind.prep
674
+ if prep is not None:
675
+ assert isinstance(prep, CommandCreation)
676
+ if not root.bind.skip or not self.has_sub(root, args):
677
+ ret = prep.func(self)
678
+ if self.check_error(ret):
679
+ return ret
680
+
681
+ if hasattr(args, root.sub_dest):
682
+ sub_dest = getattr(args, root.sub_dest)
683
+ if isinstance(sub_dest, str):
684
+ assert isinstance(root.subs, (list, tuple))
685
+ for sub in root.subs:
686
+ assert isinstance(sub, CommandArgument)
687
+ if sub.name == sub_dest:
688
+ return self.__pre(args, sub)
689
+ return 0
690
+
691
+ def run(self,
692
+ root: Optional[CommandArgument] = None,
693
+ argv: Optional[Sequence[str]] = None,
694
+ **kwargs) -> int:
695
+ '''Parse and run the command line.'''
696
+ if root is None:
697
+ root = self.root
698
+
699
+ if not isinstance(root, CommandArgument):
700
+ self.logger.debug("cannot find root")
701
+ return ENOENT
702
+
703
+ kwargs.pop("prog", None) # Please do not specify prog
704
+ if "description" in root.options: # Default use root's description
705
+ kwargs["description"] = root.options["description"]
706
+ args = self.parse(root, argv, **kwargs)
707
+ self.logger.debug("%s", args)
708
+
709
+ try:
710
+ version = self.version
711
+ if isinstance(version, str):
712
+ # Output version for the debug level. Internal log
713
+ # items are debug level only, except for errors.
714
+ self.logger.debug("version: %s", version)
715
+
716
+ ret = self.__pre(args, root)
717
+ if self.check_error(ret):
718
+ return ret
719
+ return self.__run(args, root)
720
+ except KeyboardInterrupt:
721
+ return ECANCELED
722
+ except BaseException: # pylint: disable=broad-except
723
+ self.logger.exception("Something went wrong:")
724
+ return ENOTRECOVERABLE
@@ -1,9 +1,7 @@
1
1
  # coding:utf-8
2
2
 
3
- from urllib.parse import urljoin
4
-
5
3
  __project__ = "xkits-command"
6
- __version__ = "0.1.alpha.1"
4
+ __version__ = "0.2"
7
5
  __description__ = "Command line module"
8
6
  __urlhome__ = "https://github.com/bondbox/xcommand/"
9
7
 
@@ -0,0 +1,235 @@
1
+ # coding:utf-8
2
+
3
+ from argparse import ArgumentParser
4
+ from argparse import Namespace
5
+ from argparse import _ArgumentGroup # noqa:H306
6
+ from argparse import _HelpAction
7
+ from argparse import _SubParsersAction
8
+ from typing import Dict
9
+ from typing import List
10
+ from typing import Optional
11
+ from typing import Sequence
12
+ from typing import Set
13
+ from typing import Tuple
14
+
15
+ try:
16
+ from argcomplete import autocomplete
17
+ except ModuleNotFoundError: # pragma: no cover
18
+ pass # pragma: no cover
19
+
20
+ from xkits_command.attribute import __project__
21
+ from xkits_command.attribute import __urlhome__
22
+
23
+
24
+ class Checker():
25
+
26
+ prefix_chars = "-"
27
+
28
+ @classmethod
29
+ def check_name_pos(cls, fn):
30
+ '''check positional argument name'''
31
+
32
+ def inner(self, name: str, **kwargs):
33
+ assert isinstance(name, str) and name[0] not in cls.prefix_chars, \
34
+ f"{name} is not a positional argument name"
35
+ return fn(self, name, **kwargs)
36
+
37
+ return inner
38
+
39
+ @classmethod
40
+ def check_name_opt(cls, fn):
41
+ '''check optional argument name'''
42
+
43
+ def inner(self, *name: str, **kwargs):
44
+ # 1. check short form optional argument ("-x")
45
+ # 2. check long form optional argument ("--xx")
46
+ # 3. only short form or long form or short form + long form
47
+ # 模棱两可的数据(-1可以是一个负数的位置参数)
48
+ for n in name:
49
+ assert isinstance(n, str) and n[0] in cls.prefix_chars, \
50
+ f"{n} is not an optional argument name"
51
+ return fn(self, *name, **kwargs)
52
+
53
+ return inner
54
+
55
+ @classmethod
56
+ def check_nargs_opt(cls, fn):
57
+ '''nargs hook function:
58
+ nargs < -1: using "?", 0 or 1 argument, default value
59
+ nargs = -1: using "+", arguments list, at least 1
60
+ nargs = 0: using "*", arguments list, allow to be empty
61
+ nargs = 1: redirect to "?", 0 or 1 argument
62
+ nargs > 1: N arguments list
63
+ '''
64
+
65
+ def inner(self, *args, **kwargs):
66
+ _nargs = kwargs.get("nargs", -2)
67
+ if isinstance(_nargs, int) and _nargs < 2:
68
+ _nargs = {1: "?", 0: "*", -1: "+"}.get(_nargs, "?")
69
+ kwargs.update({"nargs": _nargs})
70
+ return fn(self, *args, **kwargs)
71
+
72
+ return inner
73
+
74
+
75
+ class ArgParser(ArgumentParser):
76
+ '''Simple command-line tool based on argparse.'''
77
+
78
+ def __init__(self, # pylint: disable=R0913,R0917
79
+ argv: Optional[Sequence[str]] = None,
80
+ prog: Optional[str] = None,
81
+ usage: Optional[str] = None,
82
+ prev_parser: Optional["ArgParser"] = None,
83
+ description: Optional[str] = f"Command-line based on {__project__}.", # noqa:E501
84
+ epilog: Optional[str] = f"For more, please visit {__urlhome__}", # noqa:E501
85
+ **kwargs):
86
+ kwargs.setdefault("prog", prog)
87
+ kwargs.setdefault("usage", usage)
88
+ kwargs.setdefault("description", description)
89
+ kwargs.setdefault("epilog", epilog)
90
+ ArgumentParser.__init__(self, **kwargs)
91
+ self.__argv: Optional[Sequence[str]] = argv
92
+ self.__help_option: Dict[str, _HelpAction] = {}
93
+ self.__prev_parser: ArgParser = prev_parser or self
94
+ self.__next_parser: List[ArgParser] = []
95
+ if prev_parser is not None:
96
+ prev_parser.next_parser.append(self)
97
+
98
+ @property
99
+ def argv(self) -> Optional[Sequence[str]]:
100
+ root = self.root_parser
101
+ return root.argv if root is not self else self.__argv
102
+
103
+ @property
104
+ def prev_parser(self) -> "ArgParser":
105
+ return self.__prev_parser
106
+
107
+ @property
108
+ def next_parser(self) -> List["ArgParser"]:
109
+ return self.__next_parser
110
+
111
+ @property
112
+ def root_parser(self) -> "ArgParser":
113
+ root = self.prev_parser
114
+ while root.prev_parser != root:
115
+ root = root.prev_parser
116
+ return root
117
+
118
+ def argument_group(self,
119
+ title: Optional[str] = None,
120
+ description: Optional[str] = None,
121
+ **kwargs) -> _ArgumentGroup:
122
+ '''Find the created argument group by title, create if not exist.'''
123
+ for group in self._action_groups:
124
+ if title == group.title:
125
+ return group
126
+ return self.add_argument_group(title, description, **kwargs)
127
+
128
+ @Checker.check_name_opt
129
+ def filter_optional_name(self, *name: str) -> Sequence[str]:
130
+ '''Filter defined optional argument name.'''
131
+ option_strings: Set[str] = set()
132
+ for action in self._get_optional_actions():
133
+ option_strings.update(action.option_strings)
134
+ return [n for n in name if n not in option_strings]
135
+
136
+ @Checker.check_name_pos
137
+ def add_pos(self, name: str, **kwargs) -> None:
138
+ '''Add positional argument.'''
139
+ assert "dest" not in kwargs, \
140
+ "dest supplied twice for positional argument"
141
+ self.add_argument(name, **kwargs)
142
+
143
+ @Checker.check_name_opt
144
+ @Checker.check_nargs_opt
145
+ def add_opt(self, *name: str, **kwargs) -> None:
146
+ '''Add optional argument.'''
147
+ self.add_argument(*name, **kwargs)
148
+
149
+ @Checker.check_name_opt
150
+ def add_opt_on(self, *name: str, **kwargs) -> None:
151
+ '''Add boolean optional argument, default value is False.'''
152
+ kwargs.update({"action": "store_true"})
153
+ for key in ("type", "nargs", "const", "default", "choices"):
154
+ assert key not in kwargs, f"'{key}' is an invalid argument"
155
+ self.add_argument(*name, **kwargs)
156
+
157
+ @Checker.check_name_opt
158
+ def add_opt_off(self, *name: str, **kwargs) -> None:
159
+ '''Add boolean optional argument, default value is True.'''
160
+ kwargs.update({"action": "store_false"})
161
+ for key in ("type", "nargs", "const", "default", "choices"):
162
+ assert key not in kwargs, f"'{key}' is an invalid argument"
163
+ self.add_argument(*name, **kwargs)
164
+
165
+ def add_subparsers(self, *args, **kwargs) -> _SubParsersAction:
166
+ '''Add subparsers.'''
167
+ # subparser: cannot have multiple subparser arguments
168
+ kwargs.setdefault("title", "subcommands")
169
+ kwargs.setdefault("description", None)
170
+ kwargs.setdefault("dest", f"subcmd_{self.prog}")
171
+ kwargs.setdefault("help", None)
172
+ kwargs.setdefault("metavar", None)
173
+ return ArgumentParser.add_subparsers(self, *args, **kwargs)
174
+
175
+ def parse_args( # pylint: disable=useless-parent-delegation
176
+ self, args: Optional[Sequence[str]] = None,
177
+ namespace: Optional[Namespace] = None
178
+ ) -> Namespace:
179
+ try:
180
+ autocomplete(self) # For tab completion
181
+ except NameError: # pragma: no cover
182
+ pass # pragma: no cover
183
+ return super().parse_args(args, namespace) # type: ignore
184
+
185
+ def parse_known_args( # pylint: disable=useless-parent-delegation
186
+ self, args: Optional[Sequence[str]] = None,
187
+ namespace: Optional[Namespace] = None
188
+ ) -> Tuple[Namespace, List[str]]:
189
+ return super().parse_known_args(args, namespace) # type: ignore
190
+
191
+ def __enable_help_action(self): # pylint: disable=unused-private-member
192
+ while len(self.__help_option) > 0:
193
+ option, action = self.__help_option.popitem()
194
+ self._option_string_actions[option] = action
195
+ assert len(self.__help_option) == 0
196
+
197
+ def __disable_help_action(self): # pylint: disable=unused-private-member
198
+ assert len(self.__help_option) == 0
199
+ for option, action in self._option_string_actions.items():
200
+ if isinstance(action, _HelpAction):
201
+ self.__help_option[option] = action
202
+ for option in self.__help_option:
203
+ self._option_string_actions.pop(option)
204
+
205
+ def preparse_from_sys_argv(self) -> Namespace:
206
+ '''Preparse some arguments from sys.argv for tab completion.
207
+
208
+ When arguments contain the help option, call parse_known_args()
209
+ will print help message and exit. The command line can parse
210
+ normally.
211
+
212
+ But parameters added after calling preparse_from_sys_argv() will
213
+ not show in the help message, because the exit occurred before
214
+ adding parameters.
215
+
216
+ So, disable the help action before calling parse_known_args().
217
+ The help option will be stored, and restored after the call ends.
218
+ '''
219
+
220
+ def __dfs_enable_help_action(root: ArgParser):
221
+ root.__enable_help_action() # pylint: disable=protected-access
222
+ for _sub in root.next_parser:
223
+ __dfs_enable_help_action(_sub)
224
+
225
+ def __dfs_disable_help_action(root: ArgParser):
226
+ root.__disable_help_action() # pylint: disable=protected-access
227
+ for _sub in root.next_parser:
228
+ __dfs_disable_help_action(_sub)
229
+
230
+ try:
231
+ __dfs_disable_help_action(self.root_parser)
232
+ namespace, _ = self.root_parser.parse_known_args(self.argv)
233
+ return namespace
234
+ finally:
235
+ __dfs_enable_help_action(self.root_parser)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: xkits-command
3
- Version: 0.1a1
3
+ Version: 0.2
4
4
  Summary: Command line module
5
5
  Home-page: https://github.com/bondbox/xcommand/
6
6
  Author: Mingzhe Zou
@@ -9,13 +9,15 @@ License: GPLv2
9
9
  Project-URL: Source Code, https://github.com/bondbox/xcommand/
10
10
  Project-URL: Bug Tracker, https://github.com/bondbox/xcommand/issues
11
11
  Project-URL: Documentation, https://github.com/bondbox/xcommand/
12
- Keywords: command-line,argparse,argcomplete
12
+ Keywords: command-line,argparse,argcomplete,shell,bash,terminal
13
13
  Platform: any
14
14
  Classifier: Programming Language :: Python
15
15
  Classifier: Programming Language :: Python :: 3
16
16
  Requires-Python: >=3.8
17
17
  Description-Content-Type: text/markdown
18
18
  License-File: LICENSE
19
+ Requires-Dist: argcomplete>=3.2.1
20
+ Requires-Dist: xkits_logger>=0.1
19
21
 
20
22
  # xcommand
21
23
 
@@ -3,9 +3,12 @@ README.md
3
3
  setup.cfg
4
4
  setup.py
5
5
  xkits_command/__init__.py
6
+ xkits_command/actuator.py
6
7
  xkits_command/attribute.py
8
+ xkits_command/parser.py
7
9
  xkits_command.egg-info/PKG-INFO
8
10
  xkits_command.egg-info/SOURCES.txt
9
11
  xkits_command.egg-info/dependency_links.txt
12
+ xkits_command.egg-info/requires.txt
10
13
  xkits_command.egg-info/top_level.txt
11
14
  xkits_command.egg-info/zip-safe
@@ -0,0 +1,2 @@
1
+ argcomplete>=3.2.1
2
+ xkits_logger>=0.1
File without changes
File without changes
File without changes
File without changes