edq-utils 0.0.2__py3-none-any.whl → 0.0.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of edq-utils might be problematic. Click here for more details.
- edq/__init__.py +5 -1
- edq/cli/__init__.py +0 -0
- edq/cli/testing/__init__.py +0 -0
- edq/cli/testing/cli-test.py +42 -0
- edq/cli/version.py +27 -0
- edq/core/__init__.py +0 -0
- edq/core/argparser.py +112 -0
- edq/core/argparser_test.py +124 -0
- edq/core/log.py +101 -0
- edq/core/version.py +6 -0
- edq/py.typed +0 -0
- edq/testing/__init__.py +3 -0
- edq/testing/asserts.py +66 -0
- edq/testing/cli.py +253 -0
- edq/testing/cli_test.py +8 -0
- edq/testing/run.py +34 -13
- edq/testing/testdata/cli/tests/version_base.txt +6 -0
- edq/testing/unittest.py +10 -3
- edq/util/__init__.py +3 -0
- edq/util/dirent.py +37 -3
- edq/util/dirent_test.py +120 -3
- edq/util/json.py +92 -9
- edq/util/json_test.py +109 -2
- edq/util/pyimport.py +21 -0
- edq/util/pyimport_test.py +36 -0
- {edq_utils-0.0.2.dist-info → edq_utils-0.0.4.dist-info}/METADATA +2 -1
- edq_utils-0.0.4.dist-info/RECORD +41 -0
- edq_utils-0.0.2.dist-info/RECORD +0 -26
- {edq_utils-0.0.2.dist-info → edq_utils-0.0.4.dist-info}/WHEEL +0 -0
- {edq_utils-0.0.2.dist-info → edq_utils-0.0.4.dist-info}/licenses/LICENSE +0 -0
- {edq_utils-0.0.2.dist-info → edq_utils-0.0.4.dist-info}/top_level.txt +0 -0
edq/__init__.py
CHANGED
edq/cli/__init__.py
ADDED
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# # pylint: disable=invalid-name
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Run specified CLI test files.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import argparse
|
|
8
|
+
import sys
|
|
9
|
+
import unittest
|
|
10
|
+
|
|
11
|
+
import edq.core.argparser
|
|
12
|
+
import edq.testing.cli
|
|
13
|
+
import edq.testing.cli_test
|
|
14
|
+
|
|
15
|
+
def run_cli(args: argparse.Namespace) -> int:
|
|
16
|
+
""" Run the CLI. """
|
|
17
|
+
|
|
18
|
+
edq.testing.cli.add_test_paths(edq.testing.cli_test.CLITest, args.paths)
|
|
19
|
+
|
|
20
|
+
runner = unittest.TextTestRunner(verbosity = 2)
|
|
21
|
+
tests = unittest.defaultTestLoader.loadTestsFromTestCase(edq.testing.cli_test.CLITest)
|
|
22
|
+
results = runner.run(tests)
|
|
23
|
+
|
|
24
|
+
return len(results.errors) + len(results.failures)
|
|
25
|
+
|
|
26
|
+
def main() -> int:
|
|
27
|
+
""" Get a parser, parse the args, and call run. """
|
|
28
|
+
return run_cli(_get_parser().parse_args())
|
|
29
|
+
|
|
30
|
+
def _get_parser() -> edq.core.argparser.Parser:
|
|
31
|
+
""" Get the parser. """
|
|
32
|
+
|
|
33
|
+
parser = edq.core.argparser.get_default_parser(__doc__.strip())
|
|
34
|
+
|
|
35
|
+
parser.add_argument('paths', metavar = 'PATH',
|
|
36
|
+
type = str, nargs = '+',
|
|
37
|
+
help = 'Path to CLI test case files.')
|
|
38
|
+
|
|
39
|
+
return parser
|
|
40
|
+
|
|
41
|
+
if (__name__ == '__main__'):
|
|
42
|
+
sys.exit(main())
|
edq/cli/version.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Get the version of the EduLinq Python utils package.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
import edq.core.argparser
|
|
9
|
+
import edq.core.version
|
|
10
|
+
|
|
11
|
+
def run_cli(args: argparse.Namespace) -> int:
|
|
12
|
+
""" Run the CLI. """
|
|
13
|
+
|
|
14
|
+
print(f"v{edq.core.version.get_version()}")
|
|
15
|
+
return 0
|
|
16
|
+
|
|
17
|
+
def main() -> int:
|
|
18
|
+
""" Get a parser, parse the args, and call run. """
|
|
19
|
+
return run_cli(_get_parser().parse_args())
|
|
20
|
+
|
|
21
|
+
def _get_parser() -> edq.core.argparser.Parser:
|
|
22
|
+
""" Get the parser. """
|
|
23
|
+
|
|
24
|
+
return edq.core.argparser.get_default_parser(__doc__.strip())
|
|
25
|
+
|
|
26
|
+
if (__name__ == '__main__'):
|
|
27
|
+
sys.exit(main())
|
edq/core/__init__.py
ADDED
|
File without changes
|
edq/core/argparser.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""
|
|
2
|
+
A place to handle common CLI arguments.
|
|
3
|
+
"parsers" in this file are always assumed to be argparse parsers.
|
|
4
|
+
|
|
5
|
+
The general idea is that callers can register callbacks to be called before and after parsing CLI arguments.
|
|
6
|
+
Pre-callbacks are generally intended to add arguments to the parser,
|
|
7
|
+
while post-callbacks are generally intended to act on the results of parsing.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import typing
|
|
12
|
+
|
|
13
|
+
import edq.core.log
|
|
14
|
+
|
|
15
|
+
@typing.runtime_checkable
|
|
16
|
+
class PreParseFunction(typing.Protocol):
|
|
17
|
+
"""
|
|
18
|
+
A function that can be called before parsing arguments.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __call__(self, parser: argparse.ArgumentParser, extra_state: typing.Dict[str, typing.Any]) -> None:
|
|
22
|
+
"""
|
|
23
|
+
Prepare a parser for parsing.
|
|
24
|
+
This is generally used for adding your module's arguments to the parser,
|
|
25
|
+
for example a logging module may add arguments to set a logging level.
|
|
26
|
+
|
|
27
|
+
The extra state is shared between all pre-parse functions
|
|
28
|
+
and will be placed in the final parsed output under `_pre_extra_state_`.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
@typing.runtime_checkable
|
|
32
|
+
class PostParseFunction(typing.Protocol):
|
|
33
|
+
"""
|
|
34
|
+
A function that can be called after parsing arguments.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __call__(self,
|
|
38
|
+
parser: argparse.ArgumentParser,
|
|
39
|
+
args: argparse.Namespace,
|
|
40
|
+
extra_state: typing.Dict[str, typing.Any]) -> None:
|
|
41
|
+
"""
|
|
42
|
+
Take actions after arguments are parsed.
|
|
43
|
+
This is generally used for initializing your module with options,
|
|
44
|
+
for example a logging module may set a logging level.
|
|
45
|
+
|
|
46
|
+
The extra state is shared between all post-parse functions
|
|
47
|
+
and will be placed in the final parsed output under `_post_extra_state_`.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
class Parser(argparse.ArgumentParser):
|
|
51
|
+
"""
|
|
52
|
+
Extend an argparse parser to call the pre and post functions.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def __init__(self, *args: typing.Any, **kwargs: typing.Any) -> None:
|
|
56
|
+
super().__init__(*args, **kwargs)
|
|
57
|
+
|
|
58
|
+
self._pre_parse_callbacks: typing.Dict[str, PreParseFunction] = {}
|
|
59
|
+
self._post_parse_callbacks: typing.Dict[str, PostParseFunction] = {}
|
|
60
|
+
|
|
61
|
+
def register_callbacks(self,
|
|
62
|
+
key: str,
|
|
63
|
+
pre_parse_callback: typing.Union[PreParseFunction, None] = None,
|
|
64
|
+
post_parse_callback: typing.Union[PostParseFunction, None] = None,
|
|
65
|
+
) -> None:
|
|
66
|
+
"""
|
|
67
|
+
Register callback functions to run before/after argument parsing.
|
|
68
|
+
Any existing callbacks under the specified key will be replaced.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
if (pre_parse_callback is not None):
|
|
72
|
+
self._pre_parse_callbacks[key] = pre_parse_callback
|
|
73
|
+
|
|
74
|
+
if (post_parse_callback is not None):
|
|
75
|
+
self._post_parse_callbacks[key] = post_parse_callback
|
|
76
|
+
|
|
77
|
+
def parse_args(self, # type: ignore[override]
|
|
78
|
+
*args: typing.Any,
|
|
79
|
+
skip_keys: typing.Union[typing.List[str], None] = None,
|
|
80
|
+
**kwargs: typing.Any) -> argparse.Namespace:
|
|
81
|
+
if (skip_keys is None):
|
|
82
|
+
skip_keys = []
|
|
83
|
+
|
|
84
|
+
# Call pre-parse callbacks.
|
|
85
|
+
pre_extra_state: typing.Dict[str, typing.Any] = {}
|
|
86
|
+
for (key, pre_parse_callback) in self._pre_parse_callbacks.items():
|
|
87
|
+
if (key not in skip_keys):
|
|
88
|
+
pre_parse_callback(self, pre_extra_state)
|
|
89
|
+
|
|
90
|
+
# Parse the args.
|
|
91
|
+
parsed_args = super().parse_args(*args, **kwargs)
|
|
92
|
+
|
|
93
|
+
# Call post-parse callbacks.
|
|
94
|
+
post_extra_state: typing.Dict[str, typing.Any] = {}
|
|
95
|
+
for (key, post_parse_callback) in self._post_parse_callbacks.items():
|
|
96
|
+
if (key not in skip_keys):
|
|
97
|
+
post_parse_callback(self, parsed_args, post_extra_state)
|
|
98
|
+
|
|
99
|
+
# Attach the additional state to the args.
|
|
100
|
+
setattr(parsed_args, '_pre_extra_state_', pre_extra_state)
|
|
101
|
+
setattr(parsed_args, '_post_extra_state_', post_extra_state)
|
|
102
|
+
|
|
103
|
+
return parsed_args # type: ignore[no-any-return]
|
|
104
|
+
|
|
105
|
+
def get_default_parser(description: str) -> Parser:
|
|
106
|
+
""" Get a parser with the default callbacks already attached. """
|
|
107
|
+
|
|
108
|
+
parser = Parser(description = description)
|
|
109
|
+
|
|
110
|
+
parser.register_callbacks('log', edq.core.log.set_cli_args, edq.core.log.init_from_args)
|
|
111
|
+
|
|
112
|
+
return parser
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
|
|
3
|
+
import edq.core.argparser
|
|
4
|
+
import edq.testing.unittest
|
|
5
|
+
|
|
6
|
+
class TestArgParser(edq.testing.unittest.BaseTest):
|
|
7
|
+
""" Test argument parsing. """
|
|
8
|
+
|
|
9
|
+
def test_callbacks_base(self):
|
|
10
|
+
""" Test the argument parsing callbacks. """
|
|
11
|
+
|
|
12
|
+
# [(parse text, [(key, pre, post), ...], skip keys, expected (as dict)), ...]
|
|
13
|
+
test_cases = [
|
|
14
|
+
# Empty
|
|
15
|
+
(
|
|
16
|
+
"",
|
|
17
|
+
[],
|
|
18
|
+
[],
|
|
19
|
+
{
|
|
20
|
+
'_pre_extra_state_': {},
|
|
21
|
+
'_post_extra_state_': {},
|
|
22
|
+
},
|
|
23
|
+
),
|
|
24
|
+
|
|
25
|
+
# Single Callbacks
|
|
26
|
+
(
|
|
27
|
+
"",
|
|
28
|
+
[
|
|
29
|
+
('test', functools.partial(_pre_callback_append, value = 1), functools.partial(_post_callback_append, value = 2)),
|
|
30
|
+
],
|
|
31
|
+
[],
|
|
32
|
+
{
|
|
33
|
+
'_pre_extra_state_': {
|
|
34
|
+
'append': [1],
|
|
35
|
+
},
|
|
36
|
+
'_post_extra_state_': {
|
|
37
|
+
'append': [2],
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
),
|
|
41
|
+
|
|
42
|
+
# Double Callbacks
|
|
43
|
+
(
|
|
44
|
+
"",
|
|
45
|
+
[
|
|
46
|
+
('test1', functools.partial(_pre_callback_append, value = 1), functools.partial(_post_callback_append, value = 2)),
|
|
47
|
+
('test2', functools.partial(_pre_callback_append, value = 3), functools.partial(_post_callback_append, value = 4)),
|
|
48
|
+
],
|
|
49
|
+
[],
|
|
50
|
+
{
|
|
51
|
+
'_pre_extra_state_': {
|
|
52
|
+
'append': [1, 3],
|
|
53
|
+
},
|
|
54
|
+
'_post_extra_state_': {
|
|
55
|
+
'append': [2, 4],
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
),
|
|
59
|
+
|
|
60
|
+
# Split Callbacks
|
|
61
|
+
(
|
|
62
|
+
"",
|
|
63
|
+
[
|
|
64
|
+
('test1', functools.partial(_pre_callback_append, value = 1), None),
|
|
65
|
+
('test2', None, functools.partial(_post_callback_append, value = 4)),
|
|
66
|
+
],
|
|
67
|
+
[],
|
|
68
|
+
{
|
|
69
|
+
'_pre_extra_state_': {
|
|
70
|
+
'append': [1],
|
|
71
|
+
},
|
|
72
|
+
'_post_extra_state_': {
|
|
73
|
+
'append': [4],
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
),
|
|
77
|
+
|
|
78
|
+
# Override Callbacks
|
|
79
|
+
(
|
|
80
|
+
"",
|
|
81
|
+
[
|
|
82
|
+
('test', functools.partial(_pre_callback_append, value = 1), functools.partial(_post_callback_append, value = 2)),
|
|
83
|
+
('test', functools.partial(_pre_callback_append, value = 3), functools.partial(_post_callback_append, value = 4)),
|
|
84
|
+
],
|
|
85
|
+
[],
|
|
86
|
+
{
|
|
87
|
+
'_pre_extra_state_': {
|
|
88
|
+
'append': [3],
|
|
89
|
+
},
|
|
90
|
+
'_post_extra_state_': {
|
|
91
|
+
'append': [4],
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
),
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
for (i, test_case) in enumerate(test_cases):
|
|
98
|
+
(text, registrations, skip_keys, expected) = test_case
|
|
99
|
+
|
|
100
|
+
with self.subTest(msg = f"Case {i} ('{text}'):"):
|
|
101
|
+
parser = edq.core.argparser.Parser(f"Case {i}")
|
|
102
|
+
for (key, pre, post) in registrations:
|
|
103
|
+
parser.register_callbacks(key, pre, post)
|
|
104
|
+
|
|
105
|
+
args = parser.parse_args(text.split(), skip_keys = skip_keys)
|
|
106
|
+
|
|
107
|
+
actual = vars(args)
|
|
108
|
+
self.assertJSONDictEqual(expected, actual)
|
|
109
|
+
|
|
110
|
+
def _pre_callback_append(parser, extra_state, key = 'append', value = None) -> None:
|
|
111
|
+
""" Append the given value into the extra state. """
|
|
112
|
+
|
|
113
|
+
if (key not in extra_state):
|
|
114
|
+
extra_state[key] = []
|
|
115
|
+
|
|
116
|
+
extra_state[key].append(value)
|
|
117
|
+
|
|
118
|
+
def _post_callback_append(parser, args, extra_state, key = 'append', value = None) -> None:
|
|
119
|
+
""" Append the given value into the extra state. """
|
|
120
|
+
|
|
121
|
+
if (key not in extra_state):
|
|
122
|
+
extra_state[key] = []
|
|
123
|
+
|
|
124
|
+
extra_state[key].append(value)
|
edq/core/log.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import logging
|
|
3
|
+
import typing
|
|
4
|
+
|
|
5
|
+
DEFAULT_LOGGING_LEVEL: str = logging.getLevelName(logging.INFO)
|
|
6
|
+
DEFAULT_LOGGING_FORMAT: str = '%(asctime)s [%(levelname)-8s] - %(filename)s:%(lineno)s -- %(message)s'
|
|
7
|
+
|
|
8
|
+
LEVELS: typing.List[str] = [
|
|
9
|
+
'TRACE',
|
|
10
|
+
logging.getLevelName(logging.DEBUG),
|
|
11
|
+
logging.getLevelName(logging.INFO),
|
|
12
|
+
logging.getLevelName(logging.WARNING),
|
|
13
|
+
logging.getLevelName(logging.ERROR),
|
|
14
|
+
logging.getLevelName(logging.CRITICAL),
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
def init(level: str = DEFAULT_LOGGING_LEVEL, log_format: str = DEFAULT_LOGGING_FORMAT,
|
|
18
|
+
warn_loggers: typing.Union[typing.List[str], None] = None,
|
|
19
|
+
**kwargs: typing.Any) -> None:
|
|
20
|
+
"""
|
|
21
|
+
Initialize or re-initialize the logging infrastructure.
|
|
22
|
+
The list of warning loggers is a list of identifiers for loggers (usually third-party) to move up to warning on init.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
# Add trace.
|
|
26
|
+
_add_logging_level('TRACE', logging.DEBUG - 5)
|
|
27
|
+
|
|
28
|
+
logging.basicConfig(level = level, format = log_format, force = True)
|
|
29
|
+
|
|
30
|
+
if (warn_loggers is not None):
|
|
31
|
+
for warn_logger in warn_loggers:
|
|
32
|
+
logging.getLogger(warn_logger).setLevel(logging.WARNING)
|
|
33
|
+
|
|
34
|
+
logging.trace("Logging initialized with level '%s'.", level) # type: ignore[attr-defined]
|
|
35
|
+
|
|
36
|
+
def set_cli_args(parser: argparse.ArgumentParser, extra_state: typing.Dict[str, typing.Any]) -> None:
|
|
37
|
+
"""
|
|
38
|
+
Set common CLI arguments.
|
|
39
|
+
This is a sibling to init_from_args(), as the arguments set here can be interpreted there.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
parser.add_argument('--log-level', dest = 'log_level',
|
|
43
|
+
action = 'store', type = str, default = logging.getLevelName(logging.INFO),
|
|
44
|
+
choices = LEVELS,
|
|
45
|
+
help = 'Set the logging level (default: %(default)s).')
|
|
46
|
+
|
|
47
|
+
parser.add_argument('--quiet', dest = 'quiet',
|
|
48
|
+
action = 'store_true', default = False,
|
|
49
|
+
help = 'Set the logging level to warning (overrides --log-level) (default: %(default)s).')
|
|
50
|
+
|
|
51
|
+
parser.add_argument('--debug', dest = 'debug',
|
|
52
|
+
action = 'store_true', default = False,
|
|
53
|
+
help = 'Set the logging level to debug (overrides --log-level and --quiet) (default: %(default)s).')
|
|
54
|
+
|
|
55
|
+
def init_from_args(
|
|
56
|
+
parser: argparse.ArgumentParser,
|
|
57
|
+
args: argparse.Namespace,
|
|
58
|
+
extra_state: typing.Dict[str, typing.Any]) -> None:
|
|
59
|
+
"""
|
|
60
|
+
Take in args from a parser that was passed to set_cli_args(),
|
|
61
|
+
and call init() with the appropriate arguments.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
level = args.log_level
|
|
65
|
+
|
|
66
|
+
if (args.quiet):
|
|
67
|
+
level = logging.getLevelName(logging.WARNING)
|
|
68
|
+
|
|
69
|
+
if (args.debug):
|
|
70
|
+
level = logging.getLevelName(logging.DEBUG)
|
|
71
|
+
|
|
72
|
+
init(level)
|
|
73
|
+
|
|
74
|
+
def _add_logging_level(level_name: str, level_number: int, method_name: typing.Union[str, None] = None) -> None:
|
|
75
|
+
"""
|
|
76
|
+
Add a new logging level.
|
|
77
|
+
|
|
78
|
+
See https://stackoverflow.com/questions/2183233/how-to-add-a-custom-loglevel-to-pythons-logging-facility/35804945#35804945 .
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
if (method_name is None):
|
|
82
|
+
method_name = level_name.lower()
|
|
83
|
+
|
|
84
|
+
# Level has already been defined.
|
|
85
|
+
if hasattr(logging, level_name):
|
|
86
|
+
return
|
|
87
|
+
|
|
88
|
+
def log_for_level(self: typing.Any, message: str, *args: typing.Any, **kwargs: typing.Any) -> None:
|
|
89
|
+
if self.isEnabledFor(level_number):
|
|
90
|
+
self._log(level_number, message, args, **kwargs)
|
|
91
|
+
|
|
92
|
+
def log_to_root(message: str, *args: typing.Any, **kwargs: typing.Any) -> None:
|
|
93
|
+
logging.log(level_number, message, *args, **kwargs)
|
|
94
|
+
|
|
95
|
+
logging.addLevelName(level_number, level_name)
|
|
96
|
+
setattr(logging, level_name, level_number)
|
|
97
|
+
setattr(logging.getLoggerClass(), method_name, log_for_level)
|
|
98
|
+
setattr(logging, method_name, log_to_root)
|
|
99
|
+
|
|
100
|
+
# Load the default logging when this module is loaded.
|
|
101
|
+
init()
|
edq/core/version.py
ADDED
edq/py.typed
ADDED
|
File without changes
|
edq/testing/__init__.py
ADDED
edq/testing/asserts.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""
|
|
2
|
+
More complex testing assertions.
|
|
3
|
+
Often used as output checks in CLI tests.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import re
|
|
7
|
+
import typing
|
|
8
|
+
|
|
9
|
+
import edq.testing.unittest
|
|
10
|
+
|
|
11
|
+
TRACEBACK_LINE_REGEX: str = r'^\s*File "[^"]+", line \d+,.*$\n.*$(\n\s*[\^~]+\s*$)?'
|
|
12
|
+
TRACEBACK_LINE_REPLACEMENT: str = '<TRACEBACK_LINE>'
|
|
13
|
+
|
|
14
|
+
TEXT_NORMALIZATIONS: typing.List[typing.Tuple[str, str]] = [
|
|
15
|
+
(r'^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2},\d+ \[\S+ *\] - .*\.py:\d+ -- ', '<LOG_PREFIX> -- '),
|
|
16
|
+
(r'\d+\.\d+ seconds', '<DURATION_SECONDS>'),
|
|
17
|
+
(r'\bv\d+\.\d+\.\d+\b', '<VERSION>'),
|
|
18
|
+
(r'^\s*File "[^"]+", line \d+,.*$\n.*$(\n\s*[\^~]+\s*$)?', '<TRACEBACK_LINE>'),
|
|
19
|
+
(rf'{TRACEBACK_LINE_REPLACEMENT}(\n{TRACEBACK_LINE_REPLACEMENT})*', '<TRACEBACK>'),
|
|
20
|
+
]
|
|
21
|
+
"""
|
|
22
|
+
Normalization to make to the CLI output.
|
|
23
|
+
Formatted as: [(regex, replacement), ...]
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
@typing.runtime_checkable
|
|
27
|
+
class StringComparisonAssertion(typing.Protocol):
|
|
28
|
+
"""
|
|
29
|
+
A function that can be used as a comparison assertion for a test.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __call__(self,
|
|
33
|
+
test: edq.testing.unittest.BaseTest,
|
|
34
|
+
expected: str, actual: str,
|
|
35
|
+
**kwargs: typing.Any) -> None:
|
|
36
|
+
"""
|
|
37
|
+
Perform an assertion between expected and actual data.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def content_equals_raw(test: edq.testing.unittest.BaseTest, expected: str, actual: str, **kwargs: typing.Any) -> None:
|
|
42
|
+
""" Check for equality using a simple string comparison. """
|
|
43
|
+
|
|
44
|
+
test.assertEqual(expected, actual)
|
|
45
|
+
|
|
46
|
+
def content_equals_normalize(test: edq.testing.unittest.BaseTest, expected: str, actual: str, **kwargs: typing.Any) -> None:
|
|
47
|
+
"""
|
|
48
|
+
Perform some standard text normalizations (see TEXT_NORMALIZATIONS) before using simple string comparison.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
for (regex, replacement) in TEXT_NORMALIZATIONS:
|
|
52
|
+
expected = re.sub(regex, replacement, expected, flags = re.MULTILINE)
|
|
53
|
+
actual = re.sub(regex, replacement, actual, flags = re.MULTILINE)
|
|
54
|
+
|
|
55
|
+
content_equals_raw(test, expected, actual)
|
|
56
|
+
|
|
57
|
+
def has_content_100(test: edq.testing.unittest.BaseTest, expected: str, actual: str, **kwargs: typing.Any) -> None:
|
|
58
|
+
""" Check the that output has at least 100 characters. """
|
|
59
|
+
|
|
60
|
+
return has_content(test, expected, actual, min_length = 100)
|
|
61
|
+
|
|
62
|
+
def has_content(test: edq.testing.unittest.BaseTest, expected: str, actual: str, min_length: int = 100) -> None:
|
|
63
|
+
""" Ensure that the output has content of at least some length. """
|
|
64
|
+
|
|
65
|
+
message = f"Output does not meet minimum length of {min_length}, it is only {len(actual)}."
|
|
66
|
+
test.assertTrue((len(actual) >= min_length), msg = message)
|