edq-utils 0.2.3__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.

Files changed (88) hide show
  1. edq/__init__.py +5 -0
  2. edq/cli/__init__.py +0 -0
  3. edq/cli/__main__.py +17 -0
  4. edq/cli/config/__init__.py +3 -0
  5. edq/cli/config/__main__.py +15 -0
  6. edq/cli/config/list.py +69 -0
  7. edq/cli/http/__init__.py +3 -0
  8. edq/cli/http/__main__.py +15 -0
  9. edq/cli/http/exchange-server.py +71 -0
  10. edq/cli/http/send-exchange.py +45 -0
  11. edq/cli/http/verify-exchanges.py +38 -0
  12. edq/cli/testing/__init__.py +3 -0
  13. edq/cli/testing/__main__.py +15 -0
  14. edq/cli/testing/cli-test.py +49 -0
  15. edq/cli/version.py +28 -0
  16. edq/core/__init__.py +0 -0
  17. edq/core/argparser.py +137 -0
  18. edq/core/argparser_test.py +124 -0
  19. edq/core/config.py +268 -0
  20. edq/core/config_test.py +1038 -0
  21. edq/core/log.py +101 -0
  22. edq/core/version.py +6 -0
  23. edq/procedure/__init__.py +0 -0
  24. edq/procedure/verify_exchanges.py +85 -0
  25. edq/py.typed +0 -0
  26. edq/testing/__init__.py +3 -0
  27. edq/testing/asserts.py +65 -0
  28. edq/testing/cli.py +360 -0
  29. edq/testing/cli_test.py +15 -0
  30. edq/testing/httpserver.py +578 -0
  31. edq/testing/httpserver_test.py +424 -0
  32. edq/testing/run.py +142 -0
  33. edq/testing/testdata/cli/data/configs/empty/edq-config.json +1 -0
  34. edq/testing/testdata/cli/data/configs/simple-1/edq-config.json +4 -0
  35. edq/testing/testdata/cli/data/configs/simple-2/edq-config.json +3 -0
  36. edq/testing/testdata/cli/data/configs/value-number/edq-config.json +3 -0
  37. edq/testing/testdata/cli/tests/config/list/config_list_base.txt +16 -0
  38. edq/testing/testdata/cli/tests/config/list/config_list_config_value_number.txt +10 -0
  39. edq/testing/testdata/cli/tests/config/list/config_list_ignore_config.txt +14 -0
  40. edq/testing/testdata/cli/tests/config/list/config_list_no_config.txt +8 -0
  41. edq/testing/testdata/cli/tests/config/list/config_list_show_origin.txt +13 -0
  42. edq/testing/testdata/cli/tests/config/list/config_list_skip_header.txt +10 -0
  43. edq/testing/testdata/cli/tests/help_base.txt +9 -0
  44. edq/testing/testdata/cli/tests/platform_skip.txt +5 -0
  45. edq/testing/testdata/cli/tests/version_base.txt +6 -0
  46. edq/testing/testdata/http/exchanges/simple.httpex.json +5 -0
  47. edq/testing/testdata/http/exchanges/simple_anchor.httpex.json +5 -0
  48. edq/testing/testdata/http/exchanges/simple_file.httpex.json +10 -0
  49. edq/testing/testdata/http/exchanges/simple_file_binary.httpex.json +10 -0
  50. edq/testing/testdata/http/exchanges/simple_file_get_params.httpex.json +14 -0
  51. edq/testing/testdata/http/exchanges/simple_file_multiple.httpex.json +13 -0
  52. edq/testing/testdata/http/exchanges/simple_file_name.httpex.json +11 -0
  53. edq/testing/testdata/http/exchanges/simple_file_post_multiple.httpex.json +13 -0
  54. edq/testing/testdata/http/exchanges/simple_file_post_params.httpex.json +14 -0
  55. edq/testing/testdata/http/exchanges/simple_headers.httpex.json +8 -0
  56. edq/testing/testdata/http/exchanges/simple_jsonresponse_dict.httpex.json +7 -0
  57. edq/testing/testdata/http/exchanges/simple_jsonresponse_list.httpex.json +9 -0
  58. edq/testing/testdata/http/exchanges/simple_params.httpex.json +9 -0
  59. edq/testing/testdata/http/exchanges/simple_post.httpex.json +5 -0
  60. edq/testing/testdata/http/exchanges/simple_post_params.httpex.json +9 -0
  61. edq/testing/testdata/http/exchanges/simple_post_urlparams.httpex.json +5 -0
  62. edq/testing/testdata/http/exchanges/simple_urlparams.httpex.json +5 -0
  63. edq/testing/testdata/http/exchanges/specialcase_listparams_explicit.httpex.json +8 -0
  64. edq/testing/testdata/http/exchanges/specialcase_listparams_url.httpex.json +5 -0
  65. edq/testing/testdata/http/files/a.txt +1 -0
  66. edq/testing/testdata/http/files/tiny.png +0 -0
  67. edq/testing/unittest.py +88 -0
  68. edq/util/__init__.py +3 -0
  69. edq/util/cli.py +151 -0
  70. edq/util/dirent.py +346 -0
  71. edq/util/dirent_test.py +1004 -0
  72. edq/util/encoding.py +18 -0
  73. edq/util/hash.py +41 -0
  74. edq/util/hash_test.py +89 -0
  75. edq/util/json.py +180 -0
  76. edq/util/json_test.py +228 -0
  77. edq/util/net.py +1047 -0
  78. edq/util/parse.py +33 -0
  79. edq/util/pyimport.py +94 -0
  80. edq/util/pyimport_test.py +119 -0
  81. edq/util/reflection.py +32 -0
  82. edq/util/time.py +75 -0
  83. edq/util/time_test.py +107 -0
  84. edq_utils-0.2.3.dist-info/METADATA +164 -0
  85. edq_utils-0.2.3.dist-info/RECORD +88 -0
  86. edq_utils-0.2.3.dist-info/WHEEL +5 -0
  87. edq_utils-0.2.3.dist-info/licenses/LICENSE +21 -0
  88. edq_utils-0.2.3.dist-info/top_level.txt +1 -0
edq/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """
2
+ General Python tools used by several EduLinq projects.
3
+ """
4
+
5
+ __version__ = '0.2.3'
edq/cli/__init__.py ADDED
File without changes
edq/cli/__main__.py ADDED
@@ -0,0 +1,17 @@
1
+ """
2
+ The `edq.cli` package contains general tools for the EduLinq Python library.
3
+ Each package can be invoked to list the tools (or subpackages) it contains.
4
+ Each tool includes a help prompt that accessed with the `-h`/`--help` flag.
5
+ """
6
+
7
+ import sys
8
+
9
+ import edq.util.cli
10
+
11
+ def main() -> int:
12
+ """ List this CLI dir. """
13
+
14
+ return edq.util.cli.main()
15
+
16
+ if (__name__ == '__main__'):
17
+ sys.exit(main())
@@ -0,0 +1,3 @@
1
+ """
2
+ The edq.cli.config package provides tools for working with project configuration options.
3
+ """
@@ -0,0 +1,15 @@
1
+ """
2
+ The `edq.cli.config` package contains tools for interacting with configuration options.
3
+ """
4
+
5
+ import sys
6
+
7
+ import edq.util.cli
8
+
9
+ def main() -> int:
10
+ """ List this CLI dir. """
11
+
12
+ return edq.util.cli.main()
13
+
14
+ if (__name__ == '__main__'):
15
+ sys.exit(main())
edq/cli/config/list.py ADDED
@@ -0,0 +1,69 @@
1
+ """
2
+ List the current configuration options.
3
+ """
4
+
5
+ import argparse
6
+ import sys
7
+
8
+ import edq.core.argparser
9
+
10
+ CONFIG_FIELD_SEPARATOR: str = "\t"
11
+
12
+ def run_cli(args: argparse.Namespace) -> int:
13
+ """ Run the CLI. """
14
+
15
+ rows = []
16
+
17
+ for (key, value) in args._config.items():
18
+ row = [key, str(value)]
19
+ if (args.show_origin):
20
+ config_source_obj = args._config_sources.get(key)
21
+
22
+ origin = config_source_obj.path
23
+ if (origin is None):
24
+ origin = config_source_obj.label
25
+
26
+ row.append(origin)
27
+
28
+ rows.append(CONFIG_FIELD_SEPARATOR.join(row))
29
+
30
+ rows.sort()
31
+
32
+ if (not args.skip_header):
33
+ header = ["Key", "Value"]
34
+ if (args.show_origin):
35
+ header.append("Origin")
36
+
37
+ rows.insert(0, (CONFIG_FIELD_SEPARATOR.join(header)))
38
+
39
+ print("\n".join(rows))
40
+ return 0
41
+
42
+ def main() -> int:
43
+ """ Get a parser, parse the args, and call run. """
44
+
45
+ return run_cli(_get_parser().parse_args())
46
+
47
+ def _get_parser() -> argparse.ArgumentParser:
48
+ """ Get a parser and add addition flags. """
49
+
50
+ parser = edq.core.argparser.get_default_parser(__doc__.strip())
51
+ modify_parser(parser)
52
+
53
+ return parser
54
+
55
+ def modify_parser(parser: argparse.ArgumentParser) -> None:
56
+ """ Add this CLI's flags to the given parser. """
57
+
58
+ parser.add_argument("--show-origin", dest = 'show_origin',
59
+ action = 'store_true',
60
+ help = "Display where each configuration's value was obtained from.",
61
+ )
62
+
63
+ parser.add_argument("--skip-header", dest = 'skip_header',
64
+ action = 'store_true',
65
+ help = 'Skip headers when displaying configs.',
66
+ )
67
+
68
+ if (__name__ == '__main__'):
69
+ sys.exit(main())
@@ -0,0 +1,3 @@
1
+ """
2
+ The edq.cli.http package provides CLI utilities involving HTTP.
3
+ """
@@ -0,0 +1,15 @@
1
+ """
2
+ The `edq.cli.http` package contains tools for working with HTTP/web resources.
3
+ """
4
+
5
+ import sys
6
+
7
+ import edq.util.cli
8
+
9
+ def main() -> int:
10
+ """ List this CLI dir. """
11
+
12
+ return edq.util.cli.main()
13
+
14
+ if (__name__ == '__main__'):
15
+ sys.exit(main())
@@ -0,0 +1,71 @@
1
+ # pylint: disable=invalid-name
2
+
3
+ """
4
+ Start an HTTP test server that serves the specified HTTP exchanges.
5
+ """
6
+
7
+ import argparse
8
+ import os
9
+ import sys
10
+
11
+ import edq.core.argparser
12
+ import edq.testing.httpserver
13
+
14
+ def run_cli(args: argparse.Namespace) -> int:
15
+ """ Run the CLI. """
16
+
17
+ match_options = {
18
+ 'params_to_skip': args.ignore_params,
19
+ 'headers_to_skip': args.ignore_headers,
20
+ }
21
+
22
+ server = edq.testing.httpserver.HTTPTestServer(
23
+ port = args.port,
24
+ match_options = match_options,
25
+ verbose = True,
26
+ raise_on_404 = False,
27
+ )
28
+
29
+ for path in args.paths:
30
+ path = os.path.abspath(path)
31
+
32
+ if (os.path.isfile(path)):
33
+ server.load_exchange(path)
34
+ else:
35
+ server.load_exchanges_dir(path)
36
+
37
+ server.start_and_wait()
38
+
39
+ return 0
40
+
41
+ def main() -> int:
42
+ """ Get a parser, parse the args, and call run. """
43
+ return run_cli(_get_parser().parse_args())
44
+
45
+ def _get_parser() -> argparse.ArgumentParser:
46
+ """ Get the parser. """
47
+
48
+ parser = edq.core.argparser.get_default_parser(__doc__.strip(),
49
+ include_net = True,
50
+ )
51
+
52
+ parser.add_argument('paths', metavar = 'PATH',
53
+ type = str, nargs = '+',
54
+ help = 'Path to exchange files or dirs (which will be recursively searched for all exchange files).')
55
+
56
+ parser.add_argument('--port', dest = 'port',
57
+ action = 'store', type = int, default = None,
58
+ help = 'The port to run this test server on. If not set, a random open port will be chosen.')
59
+
60
+ parser.add_argument('--ignore-param', dest = 'ignore_params',
61
+ action = 'append', type = str, default = [],
62
+ help = 'Ignore this parameter during exchange matching.')
63
+
64
+ parser.add_argument('--ignore-header', dest = 'ignore_headers',
65
+ action = 'append', type = str, default = [],
66
+ help = 'Ignore this header during exchange matching.')
67
+
68
+ return parser
69
+
70
+ if (__name__ == '__main__'):
71
+ sys.exit(main())
@@ -0,0 +1,45 @@
1
+ # pylint: disable=invalid-name
2
+
3
+ """
4
+ Send an HTTP exchange to the target server.
5
+ """
6
+
7
+ import argparse
8
+ import sys
9
+
10
+ import edq.core.argparser
11
+ import edq.util.net
12
+
13
+ def run_cli(args: argparse.Namespace) -> int:
14
+ """ Run the CLI. """
15
+
16
+ exchange = edq.util.net.HTTPExchange.from_path(args.path)
17
+ _, body = exchange.make_request(args.server)
18
+
19
+ print(body)
20
+
21
+ return 0
22
+
23
+ def main() -> int:
24
+ """ Get a parser, parse the args, and call run. """
25
+ return run_cli(_get_parser().parse_args())
26
+
27
+ def _get_parser() -> argparse.ArgumentParser:
28
+ """ Get the parser. """
29
+
30
+ parser = edq.core.argparser.get_default_parser(__doc__.strip(),
31
+ include_net = True,
32
+ )
33
+
34
+ parser.add_argument('server', metavar = 'SERVER',
35
+ action = 'store', type = str,
36
+ help = 'Server to send the exahnge to.')
37
+
38
+ parser.add_argument('path', metavar = 'PATH',
39
+ action = 'store', type = str,
40
+ help = 'Path to the exchange file.')
41
+
42
+ return parser
43
+
44
+ if (__name__ == '__main__'):
45
+ sys.exit(main())
@@ -0,0 +1,38 @@
1
+ # pylint: disable=invalid-name
2
+
3
+ """
4
+ Verify that exchanges sent to a given server have the same response.
5
+ """
6
+
7
+ import argparse
8
+ import sys
9
+
10
+ import edq.core.argparser
11
+ import edq.procedure.verify_exchanges
12
+
13
+ def run_cli(args: argparse.Namespace) -> int:
14
+ """ Run the CLI. """
15
+
16
+ return edq.procedure.verify_exchanges.run(args.paths, args.server)
17
+
18
+ def main() -> int:
19
+ """ Get a parser, parse the args, and call run. """
20
+ return run_cli(_get_parser().parse_args())
21
+
22
+ def _get_parser() -> argparse.ArgumentParser:
23
+ """ Get the parser. """
24
+
25
+ parser = edq.core.argparser.get_default_parser(__doc__.strip())
26
+
27
+ parser.add_argument('server', metavar = 'SERVER',
28
+ action = 'store', type = str, default = None,
29
+ help = 'Address of the server to send exchanges to.')
30
+
31
+ parser.add_argument('paths', metavar = 'PATH',
32
+ type = str, nargs = '+',
33
+ help = 'Path to exchange files or dirs (which will be recursively searched for all exchange files).')
34
+
35
+ return parser
36
+
37
+ if (__name__ == '__main__'):
38
+ sys.exit(main())
@@ -0,0 +1,3 @@
1
+ """
2
+ The edq.cli.testing package provides testing-related utilities.
3
+ """
@@ -0,0 +1,15 @@
1
+ """
2
+ The `edq.cli.testing` package contains tools for testing code.
3
+ """
4
+
5
+ import sys
6
+
7
+ import edq.util.cli
8
+
9
+ def main() -> int:
10
+ """ List this CLI dir. """
11
+
12
+ return edq.util.cli.main()
13
+
14
+ if (__name__ == '__main__'):
15
+ sys.exit(main())
@@ -0,0 +1,49 @@
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.unittest
14
+
15
+ class CLITest(edq.testing.unittest.BaseTest):
16
+ """ Test CLI invocations. """
17
+
18
+ def run_cli(args: argparse.Namespace) -> int:
19
+ """ Run the CLI. """
20
+
21
+ edq.testing.cli.add_test_paths(CLITest, args.data_dir, args.paths)
22
+
23
+ runner = unittest.TextTestRunner(verbosity = 2)
24
+ tests = unittest.defaultTestLoader.loadTestsFromTestCase(CLITest)
25
+ results = runner.run(tests)
26
+
27
+ return len(results.errors) + len(results.failures)
28
+
29
+ def main() -> int:
30
+ """ Get a parser, parse the args, and call run. """
31
+ return run_cli(_get_parser().parse_args())
32
+
33
+ def _get_parser() -> argparse.ArgumentParser:
34
+ """ Get the parser. """
35
+
36
+ parser = edq.core.argparser.get_default_parser(__doc__.strip())
37
+
38
+ parser.add_argument('paths', metavar = 'PATH',
39
+ type = str, nargs = '+',
40
+ help = 'Path to CLI test case files.')
41
+
42
+ parser.add_argument('--data-dir', dest = 'data_dir',
43
+ action = 'store', type = str, default = '.',
44
+ help = 'The additional data directory (expansion of __DATA_DIR__) used for tests (default: %(default)s).')
45
+
46
+ return parser
47
+
48
+ if (__name__ == '__main__'):
49
+ sys.exit(main())
edq/cli/version.py ADDED
@@ -0,0 +1,28 @@
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
+
20
+ return run_cli(_get_parser().parse_args())
21
+
22
+ def _get_parser() -> argparse.ArgumentParser:
23
+ """ Get the parser. """
24
+
25
+ return edq.core.argparser.get_default_parser(__doc__.strip())
26
+
27
+ if (__name__ == '__main__'):
28
+ sys.exit(main())
edq/core/__init__.py ADDED
File without changes
edq/core/argparser.py ADDED
@@ -0,0 +1,137 @@
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 functools
12
+ import typing
13
+
14
+ import edq.core.config
15
+ import edq.core.log
16
+ import edq.util.net
17
+
18
+ @typing.runtime_checkable
19
+ class PreParseFunction(typing.Protocol):
20
+ """
21
+ A function that can be called before parsing arguments.
22
+ """
23
+
24
+ def __call__(self, parser: argparse.ArgumentParser, extra_state: typing.Dict[str, typing.Any]) -> None:
25
+ """
26
+ Prepare a parser for parsing.
27
+ This is generally used for adding your module's arguments to the parser,
28
+ for example a logging module may add arguments to set a logging level.
29
+
30
+ The extra state is shared between all pre-parse functions
31
+ and will be placed in the final parsed output under `_pre_extra_state_`.
32
+ """
33
+
34
+ @typing.runtime_checkable
35
+ class PostParseFunction(typing.Protocol):
36
+ """
37
+ A function that can be called after parsing arguments.
38
+ """
39
+
40
+ def __call__(self,
41
+ parser: argparse.ArgumentParser,
42
+ args: argparse.Namespace,
43
+ extra_state: typing.Dict[str, typing.Any]) -> None:
44
+ """
45
+ Take actions after arguments are parsed.
46
+ This is generally used for initializing your module with options,
47
+ for example a logging module may set a logging level.
48
+
49
+ The extra state is shared between all post-parse functions
50
+ and will be placed in the final parsed output under `_post_extra_state_`.
51
+ """
52
+
53
+ class Parser(argparse.ArgumentParser):
54
+ """
55
+ Extend an argparse parser to call the pre and post functions.
56
+ """
57
+
58
+ def __init__(self, *args: typing.Any, **kwargs: typing.Any) -> None:
59
+ super().__init__(*args, **kwargs)
60
+
61
+ self._pre_parse_callbacks: typing.Dict[str, PreParseFunction] = {}
62
+ self._post_parse_callbacks: typing.Dict[str, PostParseFunction] = {}
63
+
64
+ def register_callbacks(self,
65
+ key: str,
66
+ pre_parse_callback: typing.Union[PreParseFunction, None] = None,
67
+ post_parse_callback: typing.Union[PostParseFunction, None] = None,
68
+ ) -> None:
69
+ """
70
+ Register callback functions to run before/after argument parsing.
71
+ Any existing callbacks under the specified key will be replaced.
72
+ """
73
+
74
+ if (pre_parse_callback is not None):
75
+ self._pre_parse_callbacks[key] = pre_parse_callback
76
+
77
+ if (post_parse_callback is not None):
78
+ self._post_parse_callbacks[key] = post_parse_callback
79
+
80
+ def parse_args(self, # type: ignore[override]
81
+ *args: typing.Any,
82
+ skip_keys: typing.Union[typing.List[str], None] = None,
83
+ **kwargs: typing.Any) -> argparse.Namespace:
84
+ if (skip_keys is None):
85
+ skip_keys = []
86
+
87
+ # Call pre-parse callbacks.
88
+ pre_extra_state: typing.Dict[str, typing.Any] = {}
89
+ for (key, pre_parse_callback) in self._pre_parse_callbacks.items():
90
+ if (key not in skip_keys):
91
+ pre_parse_callback(self, pre_extra_state)
92
+
93
+ # Parse the args.
94
+ parsed_args = super().parse_args(*args, **kwargs)
95
+
96
+ # Call post-parse callbacks.
97
+ post_extra_state: typing.Dict[str, typing.Any] = {}
98
+ for (key, post_parse_callback) in self._post_parse_callbacks.items():
99
+ if (key not in skip_keys):
100
+ post_parse_callback(self, parsed_args, post_extra_state)
101
+
102
+ # Attach the additional state to the args.
103
+ setattr(parsed_args, '_pre_extra_state_', pre_extra_state)
104
+ setattr(parsed_args, '_post_extra_state_', post_extra_state)
105
+
106
+ return parsed_args # type: ignore[no-any-return]
107
+
108
+ def get_default_parser(description: str,
109
+ version: typing.Union[str, None] = None,
110
+ include_log: bool = True,
111
+ include_config: bool = True,
112
+ include_net: bool = False,
113
+ config_options: typing.Union[typing.Dict[str, typing.Any], None] = None,
114
+ ) -> Parser:
115
+ """ Get a parser with the requested default callbacks already attached. """
116
+
117
+ if (config_options is None):
118
+ config_options = {}
119
+
120
+ parser = Parser(description = description)
121
+
122
+ if (version is not None):
123
+ parser.add_argument('--version',
124
+ action = 'version', version = version)
125
+
126
+ if (include_log):
127
+ parser.register_callbacks('log', edq.core.log.set_cli_args, edq.core.log.init_from_args)
128
+
129
+ if (include_config):
130
+ config_pre_func = functools.partial(edq.core.config.set_cli_args, **config_options)
131
+ config_post_func = functools.partial(edq.core.config.load_config_into_args, **config_options)
132
+ parser.register_callbacks('config', config_pre_func, config_post_func)
133
+
134
+ if (include_net):
135
+ parser.register_callbacks('net', edq.util.net.set_cli_args, edq.util.net.init_from_args)
136
+
137
+ 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)