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.
@@ -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.
@@ -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,2 @@
1
+ # termkit
2
+ Command Line Tools with ease
@@ -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"
@@ -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,2 @@
1
+ from .core import Termkit as Termkit
2
+ from .core import run as run
@@ -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