termkit 0.0.1b0__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.
- termkit-0.0.1b0/LICENSE +28 -0
- termkit-0.0.1b0/PKG-INFO +19 -0
- termkit-0.0.1b0/README.md +2 -0
- termkit-0.0.1b0/pyproject.toml +17 -0
- termkit-0.0.1b0/setup.py +26 -0
- termkit-0.0.1b0/termkit/__init__.py +2 -0
- termkit-0.0.1b0/termkit/_formatter.py +65 -0
- termkit-0.0.1b0/termkit/_helpers.py +32 -0
- termkit-0.0.1b0/termkit/core.py +198 -0
- termkit-0.0.1b0/termkit/options.py +201 -0
- termkit-0.0.1b0/termkit/tests.py +102 -0
termkit-0.0.1b0/LICENSE
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023, Thomas Mahé
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
termkit-0.0.1b0/PKG-INFO
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: termkit
|
|
3
|
+
Version: 0.0.1b0
|
|
4
|
+
Summary: Command Line Tools with ease
|
|
5
|
+
Author: Thomas Mahé
|
|
6
|
+
Author-email: contact@tmahe.dev
|
|
7
|
+
Requires-Python: >=3.6,<4.0
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.6
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# termkit
|
|
18
|
+
Command Line Tools with ease
|
|
19
|
+
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "termkit"
|
|
3
|
+
version = "0.0.1b"
|
|
4
|
+
description = "Command Line Tools with ease"
|
|
5
|
+
authors = ["Thomas Mahé <contact@tmahe.dev>"]
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
|
|
8
|
+
[tool.poetry.dependencies]
|
|
9
|
+
python = "^3.6"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
[tool.poetry.group.dev.dependencies]
|
|
13
|
+
mkdocs-material = {version = "^9.1.8", python = ">=3.7,<4.0"}
|
|
14
|
+
|
|
15
|
+
[build-system]
|
|
16
|
+
requires = ["poetry-core"]
|
|
17
|
+
build-backend = "poetry.core.masonry.api"
|
termkit-0.0.1b0/setup.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
from setuptools import setup
|
|
3
|
+
|
|
4
|
+
packages = \
|
|
5
|
+
['termkit']
|
|
6
|
+
|
|
7
|
+
package_data = \
|
|
8
|
+
{'': ['*']}
|
|
9
|
+
|
|
10
|
+
setup_kwargs = {
|
|
11
|
+
'name': 'termkit',
|
|
12
|
+
'version': '0.0.1b0',
|
|
13
|
+
'description': 'Command Line Tools with ease',
|
|
14
|
+
'long_description': '# termkit\nCommand Line Tools with ease\n',
|
|
15
|
+
'author': 'Thomas Mahé',
|
|
16
|
+
'author_email': 'contact@tmahe.dev',
|
|
17
|
+
'maintainer': 'None',
|
|
18
|
+
'maintainer_email': 'None',
|
|
19
|
+
'url': 'None',
|
|
20
|
+
'packages': packages,
|
|
21
|
+
'package_data': package_data,
|
|
22
|
+
'python_requires': '>=3.6,<4.0',
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
setup(**setup_kwargs)
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import textwrap
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class TermkitDefaultFormatter(argparse.RawTextHelpFormatter):
|
|
6
|
+
|
|
7
|
+
def __init__(self, prog: str):
|
|
8
|
+
super().__init__(prog, width=80, max_help_position=35)
|
|
9
|
+
|
|
10
|
+
def _format_action(self, action):
|
|
11
|
+
if type(action) == argparse._SubParsersAction:
|
|
12
|
+
# inject new class variable for subcommand formatting
|
|
13
|
+
subactions = action._get_subactions()
|
|
14
|
+
invocations = [self._format_action_invocation(a) for a in subactions]
|
|
15
|
+
self._subcommand_max_length = max(len(i) for i in invocations)
|
|
16
|
+
|
|
17
|
+
if type(action) == argparse._SubParsersAction._ChoicesPseudoAction:
|
|
18
|
+
# format subcommand help line
|
|
19
|
+
subcommand = self._format_action_invocation(action) # type: str
|
|
20
|
+
width = self._subcommand_max_length
|
|
21
|
+
help_text = ""
|
|
22
|
+
if action.help:
|
|
23
|
+
help_text = self._expand_help(action)
|
|
24
|
+
|
|
25
|
+
if len(help_text) > 0:
|
|
26
|
+
first_section = " {} {} ".format(subcommand, "." * (width + 4 - len(subcommand)))
|
|
27
|
+
return "{}{}\n".format(first_section,
|
|
28
|
+
textwrap.shorten(help_text, width=80 - len(first_section), placeholder="..."),
|
|
29
|
+
width=width)
|
|
30
|
+
else:
|
|
31
|
+
return " {}\n".format(subcommand, width=width)
|
|
32
|
+
|
|
33
|
+
elif type(action) == argparse._SubParsersAction:
|
|
34
|
+
# process subcommand help section
|
|
35
|
+
msg = ''
|
|
36
|
+
for subaction in action._get_subactions():
|
|
37
|
+
msg += self._format_action(subaction)
|
|
38
|
+
return msg
|
|
39
|
+
else:
|
|
40
|
+
return super(TermkitDefaultFormatter, self)._format_action(action)
|
|
41
|
+
|
|
42
|
+
def _format_action_invocation(self, action: argparse.Action):
|
|
43
|
+
if not action.option_strings:
|
|
44
|
+
metavar, = self._metavar_formatter(action, action.dest)(1)
|
|
45
|
+
return metavar
|
|
46
|
+
else:
|
|
47
|
+
parts = []
|
|
48
|
+
if action.nargs == 0:
|
|
49
|
+
parts.extend(action.option_strings)
|
|
50
|
+
else:
|
|
51
|
+
default = action.dest.upper()
|
|
52
|
+
args_string = self._format_args(action, default)
|
|
53
|
+
for option_string in action.option_strings:
|
|
54
|
+
parts.append('%s' % option_string)
|
|
55
|
+
parts[-1] += ' %s' % args_string
|
|
56
|
+
return ', '.join(parts)
|
|
57
|
+
|
|
58
|
+
def _get_help_string(self, action):
|
|
59
|
+
help = action.help
|
|
60
|
+
if '%(default)' not in action.help:
|
|
61
|
+
if action.default is not argparse.SUPPRESS and action.default is not None:
|
|
62
|
+
defaulting_nargs = [argparse.OPTIONAL, argparse.ZERO_OR_MORE]
|
|
63
|
+
if action.option_strings or action.nargs in defaulting_nargs:
|
|
64
|
+
help += ' (default: %(default)s)'
|
|
65
|
+
return help
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
|
|
3
|
+
from termkit.options import Option
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def command_format(id: str):
|
|
7
|
+
return id.replace('_', '-').lower()
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_help(item, single_line=False, is_flag_argument=False):
|
|
11
|
+
if item.__class__.__name__ == "Termkit":
|
|
12
|
+
doc = item.description
|
|
13
|
+
elif inspect.isfunction(item):
|
|
14
|
+
doc = inspect.getdoc(item)
|
|
15
|
+
elif isinstance(item, inspect.Parameter):
|
|
16
|
+
default_type = item.annotation if not issubclass(item.annotation, inspect.Parameter.empty) else str
|
|
17
|
+
if is_flag_argument:
|
|
18
|
+
doc = "(default: %(default)s)"
|
|
19
|
+
else:
|
|
20
|
+
doc = f'{default_type.__name__.upper()}'
|
|
21
|
+
doc = "STRING" if doc == "STR" else doc
|
|
22
|
+
elif isinstance(item, Option):
|
|
23
|
+
doc = ' '
|
|
24
|
+
else:
|
|
25
|
+
doc = ' '
|
|
26
|
+
|
|
27
|
+
if doc in [None, ""]:
|
|
28
|
+
doc = ' '
|
|
29
|
+
|
|
30
|
+
if single_line and len(doc.splitlines()) > 1:
|
|
31
|
+
return doc.splitlines()[0].strip()
|
|
32
|
+
return doc.strip()
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
from termkit._formatter import TermkitDefaultFormatter
|
|
4
|
+
from termkit.options import *
|
|
5
|
+
from termkit.options import __PROFILES__
|
|
6
|
+
from termkit._helpers import command_format, get_help
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_filtered_args(arguments: argparse.Namespace):
|
|
10
|
+
args = arguments.__dict__.copy()
|
|
11
|
+
for k, v in args.copy().items():
|
|
12
|
+
if k[:9] == '_Termkit_':
|
|
13
|
+
del args[k]
|
|
14
|
+
return args
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def run(func: typing.Callable, with_named_profile=False, with_argcomplete=False):
|
|
18
|
+
if inspect.isfunction(func):
|
|
19
|
+
kit = Termkit(name=command_format(func.__name__),
|
|
20
|
+
description=get_help(func),
|
|
21
|
+
with_named_profile=with_named_profile,
|
|
22
|
+
with_argcomplete=with_argcomplete)
|
|
23
|
+
kit._add_callback(func)
|
|
24
|
+
kit._run()
|
|
25
|
+
|
|
26
|
+
elif isinstance(func, Termkit):
|
|
27
|
+
func._build_parser()
|
|
28
|
+
func._run()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Termkit:
|
|
32
|
+
_callback: typing.Callable = None
|
|
33
|
+
_cli: typing.Dict = None
|
|
34
|
+
_named_profile: str = None
|
|
35
|
+
|
|
36
|
+
_parser: argparse.ArgumentParser = None
|
|
37
|
+
_formatter: argparse.HelpFormatter = TermkitDefaultFormatter
|
|
38
|
+
_with_named_profile: bool
|
|
39
|
+
_with_argcomplete: bool
|
|
40
|
+
|
|
41
|
+
def __repr__(self):
|
|
42
|
+
return json.dumps(self._cli, default=lambda o: f"<function {o.__name__}>" if inspect.isfunction(o) else o._cli,
|
|
43
|
+
indent=2)
|
|
44
|
+
|
|
45
|
+
def __init__(self,
|
|
46
|
+
name: str = os.path.basename(sys.argv[0]),
|
|
47
|
+
description: str = None,
|
|
48
|
+
with_named_profile: bool = False,
|
|
49
|
+
with_argcomplete: bool = False,
|
|
50
|
+
formatter: argparse.HelpFormatter = TermkitDefaultFormatter):
|
|
51
|
+
self.name = name
|
|
52
|
+
self.description = description
|
|
53
|
+
self._with_named_profile = with_named_profile
|
|
54
|
+
self._with_argcomplete = with_argcomplete
|
|
55
|
+
self._cli = dict()
|
|
56
|
+
self._formatter = formatter
|
|
57
|
+
|
|
58
|
+
def _setup_profile(self):
|
|
59
|
+
parser = argparse.ArgumentParser(exit_on_error=False, add_help=False)
|
|
60
|
+
self._build_parser(parser, add_help=False)
|
|
61
|
+
args, unrecognized = parser.parse_known_args()
|
|
62
|
+
self.profile = args.__dict__.get('profile', None)
|
|
63
|
+
|
|
64
|
+
def _run(self):
|
|
65
|
+
parser = self._build_parser()
|
|
66
|
+
if self._with_argcomplete:
|
|
67
|
+
import argcomplete
|
|
68
|
+
argcomplete.autocomplete(parser)
|
|
69
|
+
args = parser.parse_args()
|
|
70
|
+
try:
|
|
71
|
+
args.__CALLABLE(**get_filtered_args(args))
|
|
72
|
+
except AttributeError:
|
|
73
|
+
if self._callback is not None:
|
|
74
|
+
self._callback(**get_filtered_args(args))
|
|
75
|
+
else:
|
|
76
|
+
parser.print_help()
|
|
77
|
+
sys.exit(0)
|
|
78
|
+
|
|
79
|
+
def _setup_named_profile_defaults(self):
|
|
80
|
+
pass
|
|
81
|
+
|
|
82
|
+
def __call__(self, *args, **kwargs):
|
|
83
|
+
self._run()
|
|
84
|
+
|
|
85
|
+
def callback(self) -> typing.Callable:
|
|
86
|
+
def _decorator(func: typing.Callable):
|
|
87
|
+
self._add_callback(func)
|
|
88
|
+
|
|
89
|
+
return _decorator
|
|
90
|
+
|
|
91
|
+
def add(self, term) -> None:
|
|
92
|
+
self._cli.update({term.name: term})
|
|
93
|
+
|
|
94
|
+
def command(self, name: str = None) -> typing.Callable:
|
|
95
|
+
def _decorator(callback: typing.Callable, name: str = name):
|
|
96
|
+
if name is None:
|
|
97
|
+
name = callback.__name__
|
|
98
|
+
self._cli.update({name: callback})
|
|
99
|
+
|
|
100
|
+
return _decorator
|
|
101
|
+
|
|
102
|
+
def default_handler(self, inspect_default):
|
|
103
|
+
if isinstance(inspect_default, FromNamedProfile):
|
|
104
|
+
print("FROM NAMED PROFILE", self.profile)
|
|
105
|
+
if self.profile is not None:
|
|
106
|
+
print('inspect_default: ', inspect_default)
|
|
107
|
+
v = __PROFILES__.get(self.profile, {})
|
|
108
|
+
for l in inspect_default.loc:
|
|
109
|
+
print(v, l)
|
|
110
|
+
if isinstance(v, dict):
|
|
111
|
+
v = v.get(l, {})
|
|
112
|
+
else:
|
|
113
|
+
v = v.get(l, inspect_default.if_unset)
|
|
114
|
+
return v
|
|
115
|
+
|
|
116
|
+
return inspect_default.if_unset
|
|
117
|
+
|
|
118
|
+
else:
|
|
119
|
+
return inspect_default
|
|
120
|
+
|
|
121
|
+
def _build_parser(self, parser=None, level=0, add_help=True, positional_args=None, required_args=None,
|
|
122
|
+
optional_args=None):
|
|
123
|
+
|
|
124
|
+
if self._with_named_profile and level:
|
|
125
|
+
self._setup_profile()
|
|
126
|
+
|
|
127
|
+
if parser is None:
|
|
128
|
+
parser = argparse.ArgumentParser(prog=self.name, add_help=False,
|
|
129
|
+
description=self.description,
|
|
130
|
+
formatter_class=self._formatter)
|
|
131
|
+
positional_args = parser.add_argument_group("Positional arguments")
|
|
132
|
+
required_args = parser.add_argument_group("Required arguments")
|
|
133
|
+
optional_args = parser.add_argument_group("Optional arguments")
|
|
134
|
+
optional_args.add_argument("-h", "--help", action="help", help="show this help message and exit")
|
|
135
|
+
|
|
136
|
+
if inspect.isfunction(self._callback):
|
|
137
|
+
_populate(parser, '_callback', self._callback, positional_args, required_args, optional_args)
|
|
138
|
+
|
|
139
|
+
if inspect.isfunction(self._cli):
|
|
140
|
+
_populate(parser, "_cli", self._cli, positional_args, required_args, optional_args)
|
|
141
|
+
|
|
142
|
+
if isinstance(self._cli, typing.Dict) and len(self._cli.keys()) > 0:
|
|
143
|
+
_parser = parser.add_subparsers(title="commands")
|
|
144
|
+
|
|
145
|
+
for c_name, c_spec in self._cli.items():
|
|
146
|
+
p = _parser.add_parser(name=c_name, help=get_help(c_spec, single_line=True),
|
|
147
|
+
description=get_help(c_spec),
|
|
148
|
+
formatter_class=self._formatter, add_help=False)
|
|
149
|
+
positional_args = p.add_argument_group("Positional arguments")
|
|
150
|
+
required_args = p.add_argument_group("Required arguments")
|
|
151
|
+
optional_args = p.add_argument_group("Optional arguments")
|
|
152
|
+
optional_args.add_argument("-h", "--help", action="help", help="show this help message and exit")
|
|
153
|
+
if isinstance(c_spec, Termkit):
|
|
154
|
+
c_spec._build_parser(parser=p, level=level + 1, add_help=add_help, positional_args=positional_args,
|
|
155
|
+
required_args=required_args,
|
|
156
|
+
optional_args=optional_args)
|
|
157
|
+
if self._with_named_profile:
|
|
158
|
+
optional_args.add_argument('--profile', help="Named Profile", dest="_Termkit__PROFILE",
|
|
159
|
+
required=False, default=None, )
|
|
160
|
+
continue
|
|
161
|
+
|
|
162
|
+
_populate(p, c_name, c_spec, positional_args, required_args, optional_args)
|
|
163
|
+
|
|
164
|
+
return parser
|
|
165
|
+
|
|
166
|
+
def _add_callback(self, func: typing.Callable):
|
|
167
|
+
self._callback = func
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _populate(parser, arg_name, arg_spec, positional_args, required_args, optional_args):
|
|
171
|
+
if callable(arg_spec):
|
|
172
|
+
parser.add_argument('--_Termkit__CALLABLE', help=argparse.SUPPRESS, default=arg_spec, required=False)
|
|
173
|
+
|
|
174
|
+
signature = inspect.signature(arg_spec)
|
|
175
|
+
|
|
176
|
+
for arg, spec in signature.parameters.items():
|
|
177
|
+
if isinstance(spec.default, MutuallyExclusiveGroup):
|
|
178
|
+
spec.default._populate(parser, arg)
|
|
179
|
+
|
|
180
|
+
elif isinstance(spec.default, Choice):
|
|
181
|
+
spec.default._populate(required_args, arg) if spec.default.required else spec.default._populate(
|
|
182
|
+
optional_args, arg)
|
|
183
|
+
|
|
184
|
+
elif isinstance(spec.default, Option):
|
|
185
|
+
if spec.default.required:
|
|
186
|
+
spec.default._populate(required_args, dest=arg)
|
|
187
|
+
else:
|
|
188
|
+
spec.default._populate(optional_args, dest=arg)
|
|
189
|
+
|
|
190
|
+
# Implicit arguments
|
|
191
|
+
elif spec.default == signature.empty:
|
|
192
|
+
default_type = spec.annotation if not issubclass(spec.annotation, inspect.Parameter.empty) else str
|
|
193
|
+
positional_args.add_argument(arg, type=default_type, help=get_help(spec))
|
|
194
|
+
else:
|
|
195
|
+
default_type = spec.annotation if not issubclass(spec.annotation, inspect.Parameter.empty) else str
|
|
196
|
+
optional_args.add_argument('--' + arg.replace('_', '-'), default=spec.default, type=default_type,
|
|
197
|
+
required=False,
|
|
198
|
+
help=get_help(spec, is_flag_argument=True), metavar=get_help(spec), dest=arg)
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import inspect
|
|
5
|
+
import typing
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
__PROFILES__ = {
|
|
10
|
+
"test": {
|
|
11
|
+
"bamboo": {
|
|
12
|
+
"name": "Overwrite"
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Choice:
|
|
19
|
+
def __init__(self, *flags, choices: typing.List, help="", required=False, default=None):
|
|
20
|
+
self.flags = flags
|
|
21
|
+
self.help = help
|
|
22
|
+
self.required = required
|
|
23
|
+
self.default = default
|
|
24
|
+
self.choices = choices
|
|
25
|
+
|
|
26
|
+
def _get_metavar(self):
|
|
27
|
+
if self.default is None:
|
|
28
|
+
return "STRING"
|
|
29
|
+
|
|
30
|
+
out = f"{type(self.default).__name__.upper()}"
|
|
31
|
+
return "STRING" if out == "STR" else out
|
|
32
|
+
|
|
33
|
+
def _get_default_type(self):
|
|
34
|
+
return str if self.default is None else type(self.default)
|
|
35
|
+
|
|
36
|
+
def get_help(self):
|
|
37
|
+
if self.default is not None:
|
|
38
|
+
return f"{self.help} (default: %(default)s)".strip()
|
|
39
|
+
return self.help
|
|
40
|
+
|
|
41
|
+
def _populate(self, parser: argparse.ArgumentParser, dest=None):
|
|
42
|
+
parser.add_argument(*self.flags,
|
|
43
|
+
choices=self.choices,
|
|
44
|
+
metavar=self._get_metavar(),
|
|
45
|
+
type=self._get_default_type(),
|
|
46
|
+
default=self.default,
|
|
47
|
+
required=self.required,
|
|
48
|
+
help=self.get_help(),
|
|
49
|
+
dest=dest.strip())
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class Option:
|
|
53
|
+
def __init__(self, *flags, help="", required=False, default=None):
|
|
54
|
+
self.flags = flags
|
|
55
|
+
self.help = help
|
|
56
|
+
self.required = required
|
|
57
|
+
self.default = default
|
|
58
|
+
|
|
59
|
+
def _get_metavar(self):
|
|
60
|
+
if self.default is None:
|
|
61
|
+
return "STRING"
|
|
62
|
+
|
|
63
|
+
out = f"{type(self.default).__name__.upper()}"
|
|
64
|
+
return "STRING" if out == "STR" else out
|
|
65
|
+
|
|
66
|
+
def _get_default_type(self):
|
|
67
|
+
return str if self.default is None else type(self.default)
|
|
68
|
+
|
|
69
|
+
def get_help(self):
|
|
70
|
+
if self.default is not None:
|
|
71
|
+
return f"{self.help} (default: %(default)s)".strip()
|
|
72
|
+
return self.help
|
|
73
|
+
|
|
74
|
+
def _populate(self, parser: argparse.ArgumentParser, dest=None):
|
|
75
|
+
parser.add_argument(*self.flags,
|
|
76
|
+
metavar=self._get_metavar(),
|
|
77
|
+
type=self._get_default_type(),
|
|
78
|
+
default=self.default,
|
|
79
|
+
required=self.required,
|
|
80
|
+
help=self.get_help(),
|
|
81
|
+
dest=dest.strip())
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class MutuallyExclusiveGroup:
|
|
85
|
+
|
|
86
|
+
def __init__(self, *args: Option, name=None, required=False):
|
|
87
|
+
self.args = args
|
|
88
|
+
self.name = name
|
|
89
|
+
self.required = required
|
|
90
|
+
|
|
91
|
+
def _populate(self, parser: argparse.ArgumentParser, group_name: str):
|
|
92
|
+
if self.name is None:
|
|
93
|
+
self.name = group_name
|
|
94
|
+
|
|
95
|
+
if self.required:
|
|
96
|
+
self.name += ' (required)'
|
|
97
|
+
|
|
98
|
+
g_parser = parser.add_argument_group(self.name)
|
|
99
|
+
parser._action_groups.pop()
|
|
100
|
+
parser._action_groups.insert(0, g_parser)
|
|
101
|
+
group = g_parser.add_mutually_exclusive_group(required=False)
|
|
102
|
+
for option in self.args:
|
|
103
|
+
option._populate(group, dest=group_name)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class FromNamedProfile:
|
|
107
|
+
def __init__(self, *loc, if_unset: typing.Any):
|
|
108
|
+
self.loc = loc
|
|
109
|
+
self.if_unset = if_unset
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class EBHelpFormatter(argparse.RawTextHelpFormatter):
|
|
113
|
+
def __init__(self, prog: str):
|
|
114
|
+
super().__init__(prog, width=80, max_help_position=35)
|
|
115
|
+
|
|
116
|
+
def _format_action(self, action):
|
|
117
|
+
if type(action) == argparse._SubParsersAction:
|
|
118
|
+
# inject new class variable for subcommand formatting
|
|
119
|
+
subactions = action._get_subactions()
|
|
120
|
+
invocations = [self._format_action_invocation(a) for a in subactions]
|
|
121
|
+
self._subcommand_max_length = max(len(i) for i in invocations)
|
|
122
|
+
|
|
123
|
+
if type(action) == argparse._SubParsersAction._ChoicesPseudoAction:
|
|
124
|
+
# format subcommand help line
|
|
125
|
+
subcommand = self._format_action_invocation(action) # type: str
|
|
126
|
+
width = self._subcommand_max_length
|
|
127
|
+
help_text = ""
|
|
128
|
+
if action.help:
|
|
129
|
+
help_text = self._expand_help(action)
|
|
130
|
+
|
|
131
|
+
if len(help_text) > 0:
|
|
132
|
+
return " {} {} {}\n".format(subcommand, "." * (width + 3 - len(subcommand)), help_text, width=width)
|
|
133
|
+
else:
|
|
134
|
+
return " {}\n".format(subcommand, width=width)
|
|
135
|
+
|
|
136
|
+
elif type(action) == argparse._SubParsersAction:
|
|
137
|
+
# process subcommand help section
|
|
138
|
+
msg = ''
|
|
139
|
+
for subaction in action._get_subactions():
|
|
140
|
+
msg += self._format_action(subaction)
|
|
141
|
+
return msg
|
|
142
|
+
else:
|
|
143
|
+
return super(EBHelpFormatter, self)._format_action(action)
|
|
144
|
+
|
|
145
|
+
def _format_action_invocation(self, action):
|
|
146
|
+
if not action.option_strings:
|
|
147
|
+
metavar, = self._metavar_formatter(action, action.dest)(1)
|
|
148
|
+
return metavar
|
|
149
|
+
else:
|
|
150
|
+
parts = []
|
|
151
|
+
if action.nargs == 0:
|
|
152
|
+
parts.extend(action.option_strings)
|
|
153
|
+
else:
|
|
154
|
+
default = action.dest.upper()
|
|
155
|
+
args_string = self._format_args(action, default)
|
|
156
|
+
for option_string in action.option_strings:
|
|
157
|
+
parts.append('%s' % option_string)
|
|
158
|
+
parts[-1] += ' %s' % args_string
|
|
159
|
+
return ', '.join(parts)
|
|
160
|
+
|
|
161
|
+
# app = Termkit("nxp-tools", help="General utility to interact with NXP Services")
|
|
162
|
+
#
|
|
163
|
+
# bamboo_app = Termkit("bamboo", help="Set of command related to Bamboo")
|
|
164
|
+
#
|
|
165
|
+
#
|
|
166
|
+
# @app.command()
|
|
167
|
+
# def test(count: int,
|
|
168
|
+
# name: str = "test"):
|
|
169
|
+
# """
|
|
170
|
+
# Simple Hello world command
|
|
171
|
+
# :param count:
|
|
172
|
+
# :param name:
|
|
173
|
+
# :return:
|
|
174
|
+
# """
|
|
175
|
+
# for e in range(count):
|
|
176
|
+
# print(name)
|
|
177
|
+
#
|
|
178
|
+
#
|
|
179
|
+
# @bamboo_app.command()
|
|
180
|
+
# def test_2(profile: str = Option(flag="--profile"),
|
|
181
|
+
# name: str = Option(flag="--name", default=FromNamedProfile("bamboo", "name", if_unset="Thomas")),
|
|
182
|
+
# auth: typing.Any = MutuallyExclusiveGroup(Option(flag='--user'),
|
|
183
|
+
# Option(flag='--access-token'))):
|
|
184
|
+
# print(f"{profile=}")
|
|
185
|
+
# print(f"{name=}")
|
|
186
|
+
#
|
|
187
|
+
#
|
|
188
|
+
# @bamboo_app.command()
|
|
189
|
+
# def test_3(name: str = "test"):
|
|
190
|
+
# """
|
|
191
|
+
# Simple command
|
|
192
|
+
# :param name:
|
|
193
|
+
# :return:
|
|
194
|
+
# """
|
|
195
|
+
# print(name)
|
|
196
|
+
#
|
|
197
|
+
#
|
|
198
|
+
# app.add(bamboo_app)
|
|
199
|
+
#
|
|
200
|
+
# if __name__ == '__main__':
|
|
201
|
+
# app()
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import contextlib
|
|
2
|
+
import io
|
|
3
|
+
import unittest
|
|
4
|
+
from unittest import mock
|
|
5
|
+
from unittest.mock import patch
|
|
6
|
+
|
|
7
|
+
import sys
|
|
8
|
+
import typing
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PassException(Exception):
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TestCase(unittest.TestCase):
|
|
16
|
+
_termkit_arguments = []
|
|
17
|
+
_termkit_expected_exception = None
|
|
18
|
+
|
|
19
|
+
_termkit_with_capture_output = False
|
|
20
|
+
|
|
21
|
+
_termkit_captured_stdout: io.StringIO = None
|
|
22
|
+
_termkit_captured_stderr: io.StringIO = None
|
|
23
|
+
termkit_expected_stdout: str = None
|
|
24
|
+
termkit_expected_stderr: str = None
|
|
25
|
+
|
|
26
|
+
def setUp(self) -> None:
|
|
27
|
+
self.maxDiff = None
|
|
28
|
+
self._termkit_captured_stdout = io.StringIO()
|
|
29
|
+
self._termkit_captured_stderr = io.StringIO()
|
|
30
|
+
self.termkit_expected_stdout: str = None
|
|
31
|
+
self.termkit_expected_stderr: str = None
|
|
32
|
+
|
|
33
|
+
def tearDown(self) -> None:
|
|
34
|
+
if self._termkit_with_capture_output:
|
|
35
|
+
self.assertEqual(self.termkit_expected_stdout, self._termkit_captured_stdout.getvalue())
|
|
36
|
+
self.assertEqual(self.termkit_expected_stderr, self._termkit_captured_stderr.getvalue())
|
|
37
|
+
self._termkit_captured_stdout = io.StringIO()
|
|
38
|
+
self._termkit_captured_stderr = io.StringIO()
|
|
39
|
+
|
|
40
|
+
@classmethod
|
|
41
|
+
def tearDownClass(cls) -> None:
|
|
42
|
+
cls._termkit_captured_stdout = io.StringIO()
|
|
43
|
+
cls._termkit_captured_stderr = io.StringIO()
|
|
44
|
+
|
|
45
|
+
def assertRaises(self, excClass, callableObj, *args, **kwargs):
|
|
46
|
+
try:
|
|
47
|
+
unittest.TestCase.assertRaises(self, PassException, callableObj, *args, **kwargs)
|
|
48
|
+
except:
|
|
49
|
+
print(f'>>> {repr(sys.exc_info()[1])}')
|
|
50
|
+
self.assertEqual(f'{repr(self._termkit_expected_exception)}', f'{repr(sys.exc_info()[1])}')
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def with_captured_output():
|
|
54
|
+
"""Marks test to expect the specified exception. Call assertRaises internally"""
|
|
55
|
+
|
|
56
|
+
def test_decorator(fn):
|
|
57
|
+
def test_decorated(self):
|
|
58
|
+
with contextlib.redirect_stdout(self._termkit_captured_stdout):
|
|
59
|
+
with contextlib.redirect_stdout(self._termkit_captured_stdout):
|
|
60
|
+
with patch.object(sys, 'argv', ['prog_name'] + self._termkit_arguments):
|
|
61
|
+
if len(self._termkit_arguments) > 0:
|
|
62
|
+
print(f'>>> call with arguments {self._termkit_arguments}')
|
|
63
|
+
else:
|
|
64
|
+
print(f'>>> call without arguments')
|
|
65
|
+
self._termkit_with_capture_output = True
|
|
66
|
+
return fn(self)
|
|
67
|
+
|
|
68
|
+
return test_decorated
|
|
69
|
+
|
|
70
|
+
return test_decorator
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def with_assert_raises(exception):
|
|
74
|
+
"""Marks test to expect the specified exception. Call assertRaises internally"""
|
|
75
|
+
|
|
76
|
+
def test_decorator(fn):
|
|
77
|
+
def test_decorated(self, *args, **kwargs):
|
|
78
|
+
self._termkit_expected_exception = exception
|
|
79
|
+
self.assertRaises(type(exception), fn, self, *args, **kwargs)
|
|
80
|
+
return self
|
|
81
|
+
|
|
82
|
+
return test_decorated
|
|
83
|
+
|
|
84
|
+
return test_decorator
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def with_arguments(*arguments: str):
|
|
88
|
+
"""Marks test to expect the specified exception. Call assertRaises internally"""
|
|
89
|
+
def test_decorator(fn):
|
|
90
|
+
def test_decorated(self, *args, **kwargs):
|
|
91
|
+
self._termkit_arguments = list(arguments)
|
|
92
|
+
return fn(self)
|
|
93
|
+
return test_decorated
|
|
94
|
+
return test_decorator
|
|
95
|
+
|
|
96
|
+
# def with_arguments(*args: str):
|
|
97
|
+
#
|
|
98
|
+
# def wrapper(func: typing.Callable, args=args):
|
|
99
|
+
# func._termkit_arguments = list(map(str, args))
|
|
100
|
+
# return func
|
|
101
|
+
#
|
|
102
|
+
# return wrapper
|