1
- #!/usr/bin/env python3
1
+ # -*- coding: utf-8 -*-
2
+ # Copyright (c) 2025, Oracle and/or its affiliates.
3
+ #
4
+ # The Universal Permissive License (UPL), Version 1.0
5
+ #
6
+ # Subject to the condition set forth below, permission is hereby granted to any
7
+ # person obtaining a copy of this software, associated documentation and/or data
8
+ # (collectively the "Software"), free of charge and under any and all copyright
9
+ # rights in the Software, and any and all patent rights owned or freely
10
+ # licensable by each licensor hereunder covering either (i) the unmodified
11
+ # Software as contributed to or provided by such licensor, or (ii) the Larger
12
+ # Works (as defined below), to deal in both
13
+ #
14
+ # (a) the Software, and
15
+ # (b) any piece of software and/or hardware listed in the
16
+ # lrgrwrks.txt file if one is included with the Software (each a "Larger
17
+ # Work" to which the Software is contributed by such licensors),
18
+ #
19
+ # without restriction, including without limitation the rights to copy, create
20
+ # derivative works of, display, perform, and distribute the Software and make,
21
+ # use, sell, offer for sale, import, export, have made, and have sold the
22
+ # Software and the Larger Work(s), and to sublicense the foregoing rights on
23
+ # either these or other terms.
24
+ #
25
+ # This license is subject to the following condition: The above copyright notice
26
+ # and either this complete permission notice or at a minimum a reference to the
27
+ # UPL must be included in all copies or substantial portions of the Software.
28
+ #
29
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
30
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
31
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
32
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
33
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
34
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
35
+ # SOFTWARE.
2
36
"""
3
- A simple sub-command library for writing rich CLIs
37
+ yo.subc: A simple sub-command library for writing rich CLIs
4
38
"""
5
39
import argparse
6
40
import collections
7
41
import typing as t
8
42
from abc import ABC
9
- from abc import abstractproperty
10
43
from abc import abstractmethod
44
+ from abc import abstractproperty
11
45
12
46
13
47
def _first_different (s1 : str , s2 : str ) -> int :
@@ -51,33 +85,37 @@ def _unique_prefixes(strings: t.Iterable[str]) -> t.Dict[str, t.List[str]]:
51
85
}
52
86
53
87
54
- class _SneakyDict (collections .UserDict ):
88
+ Tk = t .TypeVar ("Tk" )
89
+ Tv = t .TypeVar ("Tv" )
90
+
91
+
92
+ class _SneakyDict (collections .UserDict , t .Generic [Tk , Tv ]): # type: ignore
55
93
"""
56
94
A dictionary which can have "hidden" keys that only show up if you know
57
95
about them. The keys are just aliases to other keys. They show up with
58
96
"getitem" and "contains" operations, but not in list / len operations.
59
97
"""
60
98
61
- def __init__ (self , * args , ** kwargs ) :
99
+ def __init__ (self , * args : t . Any , ** kwargs : t . Any ) -> None :
62
100
super ().__init__ (* args , ** kwargs )
63
- self ._aliases = {}
101
+ self ._aliases : t . Dict [ Tk , Tk ] = {}
64
102
65
- def __getitem__ (self , key ) :
103
+ def __getitem__ (self , key : Tk ) -> t . Any :
66
104
key = self ._aliases .get (key , key )
67
105
return super ().__getitem__ (key )
68
106
69
- def __contains__ (self , key ) :
107
+ def __contains__ (self , key : t . Any ) -> bool :
70
108
key = self ._aliases .get (key , key )
71
109
return super ().__contains__ (key )
72
110
73
- def add_aliases (self , alias_map : t .Dict [str , t .List [str ]]):
111
+ def add_aliases (self , alias_map : t .Dict [Tk , t .List [Tk ]]) -> None :
74
112
alias_to_name = {a : n for n , l in alias_map .items () for a in l }
75
113
self ._aliases .update (alias_to_name )
76
114
77
115
78
116
def _wrap_subparser_aliases (
79
- option : argparse ._SubParsersAction ,
80
- alias_map : t .Dict [str , t .List [str ]]
117
+ option : argparse ._SubParsersAction , # type: ignore
118
+ alias_map : t .Dict [str , t .List [str ]],
81
119
) -> None :
82
120
"""
83
121
Unfortunately, this mucks around with an internal implementation of
@@ -94,14 +132,13 @@ def _wrap_subparser_aliases(
94
132
aliases should be hidden. Thus, use a the _SneakyDict from above to hide the
95
133
aliases.
96
134
"""
97
- new_choices = _SneakyDict (option .choices )
135
+ new_choices : _SneakyDict [ str , str ] = _SneakyDict (option .choices )
98
136
new_choices .add_aliases (alias_map )
99
137
option .choices = new_choices # type: ignore
100
138
option ._name_parser_map = option .choices
101
139
102
140
103
141
T = t .TypeVar ("T" , bound = "Command" )
104
- F = t .TypeVar ("F" , bound = argparse .HelpFormatter )
105
142
106
143
107
144
class Command (ABC ):
@@ -138,7 +175,7 @@ def main():
138
175
"""
139
176
140
177
@property
141
- def help_formatter_class (self ) -> t .Type [F ]:
178
+ def help_formatter_class (self ) -> t .Type [argparse . HelpFormatter ]:
142
179
return argparse .HelpFormatter
143
180
144
181
@abstractproperty
@@ -149,14 +186,14 @@ def name(self) -> str:
149
186
def description (self ) -> str :
150
187
"""A field or property which is used as the help/description"""
151
188
152
- def add_args (self , parser : argparse .ArgumentParser ):
189
+ def add_args (self , parser : argparse .ArgumentParser ) -> None :
153
190
pass # default is no arguments
154
191
155
192
@abstractmethod
156
193
def run (self ) -> t .Any :
157
194
"""Function which is called for this command."""
158
195
159
- def base_run (self , args : argparse .Namespace ):
196
+ def base_run (self , args : argparse .Namespace ) -> t . Any :
160
197
self .args = args
161
198
return self .run ()
162
199
@@ -248,7 +285,8 @@ def add_commands(
248
285
"""
249
286
default_set = False
250
287
subparsers = parser .add_subparsers (
251
- help = argparse .SUPPRESS , metavar = "SUB-COMMAND" ,
288
+ help = argparse .SUPPRESS ,
289
+ metavar = "SUB-COMMAND" ,
252
290
)
253
291
parser .formatter_class = argparse .RawTextHelpFormatter
254
292
to_add = list (cls .iter_commands ())
@@ -306,7 +344,8 @@ def add_commands(
306
344
for subcmd , cmdlist in subcmds .items ():
307
345
subcmd_parser = subparsers .add_parser (subcmd )
308
346
subcmd_subp = subcmd_parser .add_subparsers (
309
- title = "sub-command" , metavar = "SUB-COMMAND" ,
347
+ title = "sub-command" ,
348
+ metavar = "SUB-COMMAND" ,
310
349
)
311
350
sub_names = []
312
351
names .append (subcmd )
@@ -330,7 +369,7 @@ def add_commands(
330
369
names .extend (cmd_aliases )
331
370
aliases .update (cmd_aliases )
332
371
333
- inv_aliases = collections .defaultdict (list )
372
+ inv_aliases : t . Dict [ str , t . List [ str ]] = collections .defaultdict (list )
334
373
if shortest_prefix :
335
374
inv_aliases .update (_unique_prefixes (names ))
336
375
for name , target in aliases .items ():
@@ -356,18 +395,20 @@ def add_commands(
356
395
parser .epilog = "\n " .join (lines [:- 1 ])
357
396
358
397
if not default_set :
359
- def default_func (* args , ** kwargs ):
360
- raise Exception ('you must select a sub-command' )
398
+
399
+ def default_func (* args : t .Any , ** kwargs : t .Any ) -> None :
400
+ raise Exception ("you must select a sub-command" )
401
+
361
402
parser .set_defaults (func = default_func )
362
403
return parser
363
404
364
405
@classmethod
365
406
def main (
366
- cls ,
367
- description : str ,
368
- default : t .Optional [str ] = None ,
369
- args : t .Optional [t .List [str ]] = None ,
370
- shortest_prefix : bool = False ,
407
+ cls ,
408
+ description : str ,
409
+ default : t .Optional [str ] = None ,
410
+ args : t .Optional [t .List [str ]] = None ,
411
+ shortest_prefix : bool = False ,
371
412
) -> t .Any :
372
413
"""
373
414
Parse arguments and run the selected sub-command.
@@ -390,7 +431,9 @@ def main(
390
431
"""
391
432
parser = argparse .ArgumentParser (description = description )
392
433
cls .add_commands (
393
- parser , default = default , shortest_prefix = shortest_prefix ,
434
+ parser ,
435
+ default = default ,
436
+ shortest_prefix = shortest_prefix ,
394
437
)
395
438
ns = parser .parse_args (args = args )
396
439
return ns .func (ns )
0 commit comments