edq-utils 0.0.5__py3-none-any.whl → 0.0.6__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 (62) hide show
  1. edq/__init__.py +1 -1
  2. edq/cli/config/__init__.py +3 -0
  3. edq/cli/config/list.py +69 -0
  4. edq/cli/http/__init__.py +3 -0
  5. edq/cli/http/exchange-server.py +71 -0
  6. edq/cli/http/send-exchange.py +45 -0
  7. edq/cli/http/verify-exchanges.py +38 -0
  8. edq/cli/testing/__init__.py +3 -0
  9. edq/cli/testing/cli-test.py +8 -5
  10. edq/cli/version.py +2 -1
  11. edq/core/argparser.py +28 -3
  12. edq/core/config.py +268 -0
  13. edq/core/config_test.py +1038 -0
  14. edq/procedure/__init__.py +0 -0
  15. edq/procedure/verify_exchanges.py +85 -0
  16. edq/testing/asserts.py +0 -1
  17. edq/testing/cli.py +17 -1
  18. edq/testing/httpserver.py +553 -0
  19. edq/testing/httpserver_test.py +424 -0
  20. edq/testing/run.py +40 -10
  21. edq/testing/testdata/cli/data/configs/empty/edq-config.json +1 -0
  22. edq/testing/testdata/cli/data/configs/simple-1/edq-config.json +4 -0
  23. edq/testing/testdata/cli/data/configs/simple-2/edq-config.json +3 -0
  24. edq/testing/testdata/cli/data/configs/value-number/edq-config.json +3 -0
  25. edq/testing/testdata/cli/tests/config/list/config_list_base.txt +16 -0
  26. edq/testing/testdata/cli/tests/config/list/config_list_config_value_number.txt +10 -0
  27. edq/testing/testdata/cli/tests/config/list/config_list_ignore_config.txt +14 -0
  28. edq/testing/testdata/cli/tests/config/list/config_list_no_config.txt +8 -0
  29. edq/testing/testdata/cli/tests/config/list/config_list_show_origin.txt +13 -0
  30. edq/testing/testdata/cli/tests/config/list/config_list_skip_header.txt +10 -0
  31. edq/testing/testdata/http/exchanges/simple.httpex.json +5 -0
  32. edq/testing/testdata/http/exchanges/simple_anchor.httpex.json +5 -0
  33. edq/testing/testdata/http/exchanges/simple_file.httpex.json +10 -0
  34. edq/testing/testdata/http/exchanges/simple_file_binary.httpex.json +10 -0
  35. edq/testing/testdata/http/exchanges/simple_file_get_params.httpex.json +14 -0
  36. edq/testing/testdata/http/exchanges/simple_file_multiple.httpex.json +13 -0
  37. edq/testing/testdata/http/exchanges/simple_file_name.httpex.json +11 -0
  38. edq/testing/testdata/http/exchanges/simple_file_post_multiple.httpex.json +13 -0
  39. edq/testing/testdata/http/exchanges/simple_file_post_params.httpex.json +14 -0
  40. edq/testing/testdata/http/exchanges/simple_headers.httpex.json +8 -0
  41. edq/testing/testdata/http/exchanges/simple_jsonresponse_dict.httpex.json +7 -0
  42. edq/testing/testdata/http/exchanges/simple_jsonresponse_list.httpex.json +9 -0
  43. edq/testing/testdata/http/exchanges/simple_params.httpex.json +9 -0
  44. edq/testing/testdata/http/exchanges/simple_post.httpex.json +5 -0
  45. edq/testing/testdata/http/exchanges/simple_post_params.httpex.json +9 -0
  46. edq/testing/testdata/http/exchanges/simple_post_urlparams.httpex.json +5 -0
  47. edq/testing/testdata/http/exchanges/simple_urlparams.httpex.json +5 -0
  48. edq/testing/testdata/http/exchanges/specialcase_listparams_explicit.httpex.json +8 -0
  49. edq/testing/testdata/http/exchanges/specialcase_listparams_url.httpex.json +5 -0
  50. edq/testing/testdata/http/files/a.txt +1 -0
  51. edq/testing/testdata/http/files/tiny.png +0 -0
  52. edq/testing/unittest.py +12 -7
  53. edq/util/dirent.py +2 -0
  54. edq/util/json.py +21 -4
  55. edq/util/net.py +894 -0
  56. edq_utils-0.0.6.dist-info/METADATA +156 -0
  57. edq_utils-0.0.6.dist-info/RECORD +78 -0
  58. edq_utils-0.0.5.dist-info/METADATA +0 -63
  59. edq_utils-0.0.5.dist-info/RECORD +0 -34
  60. {edq_utils-0.0.5.dist-info → edq_utils-0.0.6.dist-info}/WHEEL +0 -0
  61. {edq_utils-0.0.5.dist-info → edq_utils-0.0.6.dist-info}/licenses/LICENSE +0 -0
  62. {edq_utils-0.0.5.dist-info → edq_utils-0.0.6.dist-info}/top_level.txt +0 -0
edq/__init__.py CHANGED
@@ -2,4 +2,4 @@
2
2
  General Python tools used by several EduLinq projects.
3
3
  """
4
4
 
5
- __version__ = '0.0.5'
5
+ __version__ = '0.0.6'
@@ -0,0 +1,3 @@
1
+ """
2
+ The edq.cli.config package provides tools for working with project configuration options.
3
+ """
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,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
+ """
@@ -1,4 +1,4 @@
1
- # # pylint: disable=invalid-name
1
+ # pylint: disable=invalid-name
2
2
 
3
3
  """
4
4
  Run specified CLI test files.
@@ -10,15 +10,18 @@ import unittest
10
10
 
11
11
  import edq.core.argparser
12
12
  import edq.testing.cli
13
- import edq.testing.cli_test
13
+ import edq.testing.unittest
14
+
15
+ class CLITest(edq.testing.unittest.BaseTest):
16
+ """ Test CLI invocations. """
14
17
 
15
18
  def run_cli(args: argparse.Namespace) -> int:
16
19
  """ Run the CLI. """
17
20
 
18
- edq.testing.cli.add_test_paths(edq.testing.cli_test.CLITest, args.data_dir, args.paths)
21
+ edq.testing.cli.add_test_paths(CLITest, args.data_dir, args.paths)
19
22
 
20
23
  runner = unittest.TextTestRunner(verbosity = 2)
21
- tests = unittest.defaultTestLoader.loadTestsFromTestCase(edq.testing.cli_test.CLITest)
24
+ tests = unittest.defaultTestLoader.loadTestsFromTestCase(CLITest)
22
25
  results = runner.run(tests)
23
26
 
24
27
  return len(results.errors) + len(results.failures)
@@ -27,7 +30,7 @@ def main() -> int:
27
30
  """ Get a parser, parse the args, and call run. """
28
31
  return run_cli(_get_parser().parse_args())
29
32
 
30
- def _get_parser() -> edq.core.argparser.Parser:
33
+ def _get_parser() -> argparse.ArgumentParser:
31
34
  """ Get the parser. """
32
35
 
33
36
  parser = edq.core.argparser.get_default_parser(__doc__.strip())
edq/cli/version.py CHANGED
@@ -16,9 +16,10 @@ def run_cli(args: argparse.Namespace) -> int:
16
16
 
17
17
  def main() -> int:
18
18
  """ Get a parser, parse the args, and call run. """
19
+
19
20
  return run_cli(_get_parser().parse_args())
20
21
 
21
- def _get_parser() -> edq.core.argparser.Parser:
22
+ def _get_parser() -> argparse.ArgumentParser:
22
23
  """ Get the parser. """
23
24
 
24
25
  return edq.core.argparser.get_default_parser(__doc__.strip())
edq/core/argparser.py CHANGED
@@ -8,9 +8,12 @@ while post-callbacks are generally intended to act on the results of parsing.
8
8
  """
9
9
 
10
10
  import argparse
11
+ import functools
11
12
  import typing
12
13
 
14
+ import edq.core.config
13
15
  import edq.core.log
16
+ import edq.util.net
14
17
 
15
18
  @typing.runtime_checkable
16
19
  class PreParseFunction(typing.Protocol):
@@ -102,11 +105,33 @@ class Parser(argparse.ArgumentParser):
102
105
 
103
106
  return parsed_args # type: ignore[no-any-return]
104
107
 
105
- def get_default_parser(description: str) -> Parser:
106
- """ Get a parser with the default callbacks already attached. """
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 = {}
107
119
 
108
120
  parser = Parser(description = description)
109
121
 
110
- parser.register_callbacks('log', edq.core.log.set_cli_args, edq.core.log.init_from_args)
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)
111
136
 
112
137
  return parser
edq/core/config.py ADDED
@@ -0,0 +1,268 @@
1
+ import argparse
2
+ import os
3
+ import typing
4
+
5
+ import platformdirs
6
+
7
+ import edq.util.dirent
8
+ import edq.util.json
9
+
10
+ CONFIG_SOURCE_GLOBAL: str = "<global config file>"
11
+ CONFIG_SOURCE_LOCAL: str = "<local config file>"
12
+ CONFIG_SOURCE_CLI_FILE: str = "<cli config file>"
13
+ CONFIG_SOURCE_CLI: str = "<cli argument>"
14
+
15
+ CONFIG_PATHS_KEY: str = 'config_paths'
16
+ CONFIGS_KEY: str = 'configs'
17
+ GLOBAL_CONFIG_KEY: str = 'global_config_path'
18
+ IGNORE_CONFIGS_KEY: str = 'ignore_configs'
19
+ DEFAULT_CONFIG_FILENAME: str = "edq-config.json"
20
+
21
+ class ConfigSource:
22
+ """ A class for storing config source information. """
23
+
24
+ def __init__(self, label: str, path: typing.Union[str, None] = None) -> None:
25
+ self.label = label
26
+ """ The label identifying the config (see CONFIG_SOURCE_* constants). """
27
+
28
+ self.path = path
29
+ """ The path of where the config was sourced from. """
30
+
31
+ def __eq__(self, other: object) -> bool:
32
+ if (not isinstance(other, ConfigSource)):
33
+ return False
34
+
35
+ return ((self.label == other.label) and (self.path == other.path))
36
+
37
+ def __str__(self) -> str:
38
+ return f"({self.label}, {self.path})"
39
+
40
+ def get_global_config_path(config_filename: str) -> str:
41
+ """ Get the path for the global config file. """
42
+
43
+ return platformdirs.user_config_dir(config_filename)
44
+
45
+ def get_tiered_config(
46
+ config_filename: str = DEFAULT_CONFIG_FILENAME,
47
+ legacy_config_filename: typing.Union[str, None] = None,
48
+ cli_arguments: typing.Union[dict, argparse.Namespace, None] = None,
49
+ local_config_root_cutoff: typing.Union[str, None] = None,
50
+ ) -> typing.Tuple[typing.Dict[str, str], typing.Dict[str, ConfigSource]]:
51
+ """
52
+ Load all configuration options from files and command-line arguments.
53
+ Returns a configuration dictionary with the values based on tiering rules and a source dictionary mapping each key to its origin.
54
+ """
55
+
56
+ if (cli_arguments is None):
57
+ cli_arguments = {}
58
+
59
+ config: typing.Dict[str, str] = {}
60
+ sources: typing.Dict[str, ConfigSource] = {}
61
+
62
+ # Ensure CLI arguments are always a dict, even if provided as argparse.Namespace.
63
+ if (isinstance(cli_arguments, argparse.Namespace)):
64
+ cli_arguments = vars(cli_arguments)
65
+
66
+ global_config_path = cli_arguments.get(GLOBAL_CONFIG_KEY, get_global_config_path(config_filename))
67
+
68
+ # Check the global user config file.
69
+ if (os.path.isfile(global_config_path)):
70
+ _load_config_file(global_config_path, config, sources, CONFIG_SOURCE_GLOBAL)
71
+
72
+ # Check the local user config file.
73
+ local_config_path = _get_local_config_path(
74
+ config_filename = config_filename,
75
+ legacy_config_filename = legacy_config_filename,
76
+ local_config_root_cutoff = local_config_root_cutoff,
77
+ )
78
+
79
+ if (local_config_path is not None):
80
+ _load_config_file(local_config_path, config, sources, CONFIG_SOURCE_LOCAL)
81
+
82
+ # Check the config file specified on the command-line.
83
+ config_paths = cli_arguments.get(CONFIG_PATHS_KEY, [])
84
+ for path in config_paths:
85
+ _load_config_file(path, config, sources, CONFIG_SOURCE_CLI_FILE)
86
+
87
+ # Check the command-line config options.
88
+ cli_configs = cli_arguments.get(CONFIGS_KEY, [])
89
+ for cli_config in cli_configs:
90
+ if ("=" not in cli_config):
91
+ raise ValueError(
92
+ f"Invalid configuration option '{cli_config}'."
93
+ + " Configuration options must be provided in the format `<key>=<value>` when passed via the CLI."
94
+ )
95
+
96
+ (key, value) = cli_config.split("=", maxsplit = 1)
97
+
98
+ key = key.strip()
99
+ if (key == ""):
100
+ raise ValueError(f"Found an empty configuration option key associated with the value '{value}'.")
101
+
102
+ config[key] = value
103
+ sources[key] = ConfigSource(label = CONFIG_SOURCE_CLI)
104
+
105
+ # Finally, ignore any configs that is specified from CLI command.
106
+ cli_ignore_configs = cli_arguments.get(IGNORE_CONFIGS_KEY, [])
107
+ for ignore_config in cli_ignore_configs:
108
+ config.pop(ignore_config, None)
109
+ sources.pop(ignore_config, None)
110
+
111
+ return config, sources
112
+
113
+ def _load_config_file(
114
+ config_path: str,
115
+ config: typing.Dict[str, str],
116
+ sources: typing.Dict[str, ConfigSource],
117
+ source_label: str,
118
+ ) -> None:
119
+ """ Loads config variables and the source from the given config JSON file. """
120
+
121
+ config_path = os.path.abspath(config_path)
122
+ for (key, value) in edq.util.json.load_path(config_path).items():
123
+ key = key.strip()
124
+ if (key == ""):
125
+ raise ValueError(f"Found an empty configuration option key associated with the value '{value}'.")
126
+
127
+ config[key] = value
128
+ sources[key] = ConfigSource(label = source_label, path = config_path)
129
+
130
+ def _get_local_config_path(
131
+ config_filename: str,
132
+ legacy_config_filename: typing.Union[str, None] = None,
133
+ local_config_root_cutoff: typing.Union[str, None] = None,
134
+ ) -> typing.Union[str, None]:
135
+ """
136
+ Search for a config file in hierarchical order.
137
+ Begins with the provided config file name,
138
+ optionally checks the legacy config file name if specified,
139
+ then continues up the directory tree looking for the provided config file name.
140
+ Returns the path to the first config file found.
141
+
142
+ If no config file is found, returns None.
143
+
144
+ The cutoff parameter limits the search depth, preventing detection of config file in higher-level directories during testing.
145
+ """
146
+
147
+ # Provided config file is in current directory.
148
+ if (os.path.isfile(config_filename)):
149
+ return os.path.abspath(config_filename)
150
+
151
+ # Provided legacy config file is in current directory.
152
+ if (legacy_config_filename is not None):
153
+ if (os.path.isfile(legacy_config_filename)):
154
+ return os.path.abspath(legacy_config_filename)
155
+
156
+ # Provided config file is found in an ancestor directory up to the root or cutoff limit.
157
+ parent_dir = os.path.dirname(os.getcwd())
158
+ return _get_ancestor_config_file_path(
159
+ parent_dir,
160
+ config_filename = config_filename,
161
+ local_config_root_cutoff = local_config_root_cutoff,
162
+ )
163
+
164
+ def _get_ancestor_config_file_path(
165
+ current_directory: str,
166
+ config_filename: str,
167
+ local_config_root_cutoff: typing.Union[str, None] = None,
168
+ ) -> typing.Union[str, None]:
169
+ """
170
+ Search through the parent directories (until root or a given cutoff directory(inclusive)) for a config file.
171
+ Stops at the first occurrence of the specified config file along the path to root.
172
+ Returns the path if a config file is found.
173
+ Otherwise, returns None.
174
+ """
175
+
176
+ if (local_config_root_cutoff is not None):
177
+ local_config_root_cutoff = os.path.abspath(local_config_root_cutoff)
178
+
179
+ current_directory = os.path.abspath(current_directory)
180
+ for _ in range(edq.util.dirent.DEPTH_LIMIT):
181
+ config_file_path = os.path.join(current_directory, config_filename)
182
+ if (os.path.isfile(config_file_path)):
183
+ return config_file_path
184
+
185
+ # Check if current directory is root.
186
+ parent_dir = os.path.dirname(current_directory)
187
+ if (parent_dir == current_directory):
188
+ break
189
+
190
+ if (local_config_root_cutoff == current_directory):
191
+ break
192
+
193
+ current_directory = parent_dir
194
+
195
+ return None
196
+
197
+ def set_cli_args(parser: argparse.ArgumentParser, extra_state: typing.Dict[str, typing.Any],
198
+ config_filename: str = DEFAULT_CONFIG_FILENAME,
199
+ **kwargs: typing.Any,
200
+ ) -> None:
201
+ """
202
+ Set common CLI arguments for configuration.
203
+ """
204
+
205
+ parser.add_argument('--config-global', dest = GLOBAL_CONFIG_KEY,
206
+ action = 'store', type = str, default = get_global_config_path(config_filename),
207
+ help = 'Set the default global config file path (default: %(default)s).',
208
+ )
209
+
210
+ parser.add_argument('--config-file', dest = CONFIG_PATHS_KEY,
211
+ action = 'append', type = str, default = [],
212
+ help = ('Load config options from a JSON file.'
213
+ + ' This flag can be specified multiple times.'
214
+ + ' Files are applied in the order provided and later files override earlier ones.'
215
+ + ' Will override options form both global and local config files.')
216
+ )
217
+
218
+ parser.add_argument('--config', dest = CONFIGS_KEY,
219
+ action = 'append', type = str, default = [],
220
+ help = ('Set a configuration option from the command-line.'
221
+ + ' Specify options as <key>=<value> pairs.'
222
+ + ' This flag can be specified multiple times.'
223
+ + ' The options are applied in the order provided and later options override earlier ones.'
224
+ + ' Will override options form all config files.')
225
+ )
226
+
227
+ parser.add_argument('--ignore-config-option', dest = IGNORE_CONFIGS_KEY,
228
+ action = 'append', type = str, default = [],
229
+ help = ('Ignore any config option with the specified key.'
230
+ + ' The system-provided default value will be used for that option if one exists.'
231
+ + ' This flag can be specified multiple times.'
232
+ + ' Ignored options are processed last.')
233
+ )
234
+
235
+ def load_config_into_args(
236
+ parser: argparse.ArgumentParser,
237
+ args: argparse.Namespace,
238
+ extra_state: typing.Dict[str, typing.Any],
239
+ config_filename: str = DEFAULT_CONFIG_FILENAME,
240
+ cli_arg_config_map: typing.Union[typing.Dict[str, str], None] = None,
241
+ **kwargs: typing.Any,
242
+ ) -> None:
243
+ """
244
+ Take in args from a parser that was passed to set_cli_args(),
245
+ and get the tired configuration with the appropriate parameters, and attache it to args.
246
+
247
+ Arguments that appear on the CLI as flags (e.g. `--foo bar`) can be copied over to the config options via `cli_arg_config_map`.
248
+ The keys of `cli_arg_config_map` represent attributes in the CLI arguments (`args`),
249
+ while the values represent the desired config name this argument should be set as.
250
+ For example, a `cli_arg_config_map` of `{'foo': 'baz'}` will make the CLI argument `--foo bar`
251
+ be equivalent to `--config baz=bar`.
252
+ """
253
+
254
+ if (cli_arg_config_map is None):
255
+ cli_arg_config_map = {}
256
+
257
+ for (cli_key, config_key) in cli_arg_config_map.items():
258
+ value = getattr(args, cli_key, None)
259
+ if (value is not None):
260
+ getattr(args, CONFIGS_KEY).append(f"{config_key}={value}")
261
+
262
+ (config_dict, sources_dict) = get_tiered_config(
263
+ cli_arguments = args,
264
+ config_filename = config_filename,
265
+ )
266
+
267
+ setattr(args, "_config", config_dict)
268
+ setattr(args, "_config_sources", sources_dict)