gridparse 1.5.0__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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Georgios Chochlakis
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,3 @@
1
+ include README.md
2
+ include LICENSE
3
+ recursive-include gridparse/data *
@@ -0,0 +1,190 @@
1
+ Metadata-Version: 2.2
2
+ Name: gridparse
3
+ Version: 1.5.0
4
+ Summary: Grid search directly from argparse
5
+ Home-page: https://github.com/gchochla/gridparse
6
+ Author: Georgios Chochlakis
7
+ Author-email: "Georgios (Yiorgos) Chochlakis" <georgioschochlakis@gmail.com>
8
+ Maintainer-email: "Georgios (Yiorgos) Chochlakis" <georgioschochlakis@gmail.com>
9
+ License: MIT License
10
+
11
+ Copyright (c) 2023 Georgios Chochlakis
12
+
13
+ Permission is hereby granted, free of charge, to any person obtaining a copy
14
+ of this software and associated documentation files (the "Software"), to deal
15
+ in the Software without restriction, including without limitation the rights
16
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17
+ copies of the Software, and to permit persons to whom the Software is
18
+ furnished to do so, subject to the following conditions:
19
+
20
+ The above copyright notice and this permission notice shall be included in all
21
+ copies or substantial portions of the Software.
22
+
23
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
29
+ SOFTWARE.
30
+ Project-URL: Homepage, https://github.com/gchochla/gridparse
31
+ Project-URL: Bug Reports, https://github.com/gchochla/gridparse/issues
32
+ Project-URL: Source, https://github.com/gchochla/gridparse
33
+ Keywords: machine learning,grid search,argparse
34
+ Classifier: Development Status :: 3 - Alpha
35
+ Classifier: License :: OSI Approved :: MIT License
36
+ Classifier: Programming Language :: Python :: 3
37
+ Classifier: Programming Language :: Python :: 3.7
38
+ Classifier: Programming Language :: Python :: 3.8
39
+ Classifier: Programming Language :: Python :: 3.9
40
+ Classifier: Programming Language :: Python :: 3.10
41
+ Classifier: Programming Language :: Python :: 3.11
42
+ Classifier: Programming Language :: Python :: 3 :: Only
43
+ Requires-Python: >=3.7
44
+ Description-Content-Type: text/markdown
45
+ License-File: LICENSE
46
+ Requires-Dist: omegaconf
47
+ Provides-Extra: dev
48
+ Requires-Dist: black; extra == "dev"
49
+ Requires-Dist: pytest; extra == "dev"
50
+ Dynamic: author
51
+ Dynamic: home-page
52
+
53
+ # GridParse
54
+
55
+ A lightweight (only dependency is `omegaconf` which also downloads `yaml`) `ArgumentParser` --- aka `GridArgumentParser` --- that supports your *grid-search* needs. Supports top-level parser and subparsers. Configuration files of any type (using `omegaconf`) as also support through the argument `--gridparse-config` (also available with underscore `_`), where multiple configuration files can be passed and parsed.
56
+
57
+ ## Overview
58
+
59
+ It transforms the following arguments in the corresponding way:
60
+
61
+ `--arg 1` &rarr; `--arg 1 2 3`
62
+
63
+ `--arg 1 2 3` &rarr; `--arg 1~~2~~3 4~~5~~6`
64
+
65
+ `--arg 1-2-3 4-5-6` &rarr; `--arg 1-2-3~~4-5-6 7-8-9~~10-11`
66
+
67
+ So, for single arguments, it extends them similar to nargs="+". For multiple arguments, it extends them with `list_as_dashed_str(type, delimiter="~~")` (available in `gridparse.utils`), and this is recursively applied with existing `list_as_dashed_str` types. It can also handle subspaces using square brackets, where you can enclose combinations of hyperparameters within but don't have them combine with values of hyperparameters in other subspaces of the same length.
68
+
69
+ *Note*: when using at least on searchable argument, the return value of `parse_args()` is always a list of Namespaces, otherwise it is just a Namespace.
70
+
71
+ ## Examples
72
+
73
+ Example without subspaces:
74
+
75
+ ```python
76
+ parser = GridArgumentParser()
77
+ parser.add_argument("--hparam1", type=int, searchable=True)
78
+ parser.add_argument("--hparam2", nargs="+", type=int, searchable=True)
79
+ parser.add_argument("--normal", required=True, type=str)
80
+ parser.add_argument(
81
+ "--lists",
82
+ required=True,
83
+ nargs="+",
84
+ type=list_as_dashed_str(str),
85
+ searchable=True,
86
+ )
87
+ parser.add_argument(
88
+ "--normal_lists",
89
+ required=True,
90
+ nargs="+",
91
+ type=list_as_dashed_str(str),
92
+ )
93
+ args = parser.parse_args(
94
+ (
95
+ "--hparam1 1~~2~~3 --hparam2 4~~3 5~~4 6~~5 "
96
+ "--normal efrgthytfgn --lists 1-2-3 3-4-5~~6-7 "
97
+ "--normal_lists 1-2-3 4-5-6"
98
+ ).split()
99
+ )
100
+ assert len(args) == 1 * 3 * 1 * 2 * 1 # corresponding number of different values in input CL arguments
101
+
102
+ pprint(args)
103
+ ```
104
+
105
+ Output:
106
+
107
+ ```python
108
+ [
109
+
110
+ Namespace(hparam1=[1, 2, 3], hparam2=[4, 3], lists=[['1', '2', '3']], normal='efrgthytfgn', normal_lists=[['1', '2', '3'], ['4', '5', '6']]),
111
+
112
+ Namespace(hparam1=[1, 2, 3], hparam2=[5, 4], lists=[['1', '2', '3']], normal='efrgthytfgn', normal_lists=[['1', '2', '3'], ['4', '5', '6']]),
113
+
114
+ Namespace(hparam1=[1, 2, 3], hparam2=[6, 5], lists=[['1', '2', '3']], normal='efrgthytfgn', normal_lists=[['1', '2', '3'], ['4', '5', '6']]),
115
+
116
+ Namespace(hparam1=[1, 2, 3], hparam2=[4, 3], lists=[['3', '4', '5'], ['6', '7']], normal='efrgthytfgn', normal_lists=[['1', '2', '3'], ['4', '5', '6']]),
117
+
118
+ Namespace(hparam1=[1, 2, 3], hparam2=[5, 4], lists=[['3', '4', '5'], ['6', '7']], normal='efrgthytfgn', normal_lists=[['1', '2', '3'], ['4', '5', '6']]),
119
+
120
+ Namespace(hparam1=[1, 2, 3], hparam2=[6, 5], lists=[['3', '4', '5'], ['6', '7']], normal='efrgthytfgn', normal_lists=[['1', '2', '3'], ['4', '5', '6']])
121
+
122
+ ]
123
+ ```
124
+
125
+ Example with subspaces:
126
+
127
+ ```python
128
+ parser = GridArgumentParser()
129
+ parser.add_argument("--hparam1", type=int, searchable=True)
130
+ parser.add_argument("--hparam2", type=int, searchable=True)
131
+ parser.add_argument("--hparam3", type=int, searchable=True, default=1000)
132
+ parser.add_argument("--hparam4", type=int, searchable=True, default=2000)
133
+ parser.add_argument("--normal", required=True, type=str)
134
+
135
+ args = parser.parse_args(
136
+ (
137
+ "--hparam1 1 2 "
138
+ "{--hparam2 1 2 3 {--normal normal --hparam4 100 101 102} {--normal maybe --hparam4 200 201 202 203}} "
139
+ "{--hparam2 4 5 6 --normal not-normal}"
140
+ ).split()
141
+ )
142
+ assert len(args) == 2 * ((3 * (3 + 4)) + 3)
143
+ ```
144
+
145
+ ## Additional capabilities
146
+
147
+ ### Configuration files
148
+
149
+ Using `omegaconf` (the only dependency), we allow users to specify (potentially multiple) configuration files that can be used to populate the resulting namespace(s). Access the through the `gridparse-config` argument: `--gridparse-config /this/config.json /that/config.yml`. Command-line arguments are given higher priority, and then the priority is in order of appearance in the command line for the configuration files.
150
+
151
+ ### Specify `None` in command-line
152
+
153
+ In case some parameter is searchable (and not a boolean), you might need one of the values to be the default value `None`. In that case, specifying any other value would rule the value `None` out from the grid search. To avoid this, `gridparse` allows you to specify the value `_None_` in the command line:
154
+
155
+ ```python
156
+ >>> parser = gridparse.GridArgumentParser()
157
+ >>> parser.add_argument('--text', type=str, searchable=True)
158
+ >>> parser.parse_args("--text a b _None_".split())
159
+ [Namespace(text='a'), Namespace(text='b'), Namespace(text=None)]
160
+ ```
161
+
162
+ ### Access values of other parameter
163
+
164
+ Moreover, you can use the value (not the default) of another argument as the default by setting the default to `args.<name-of-other-argument>`.
165
+
166
+ ```python
167
+ >>> parser = gridparse.GridArgumentParser()
168
+ >>> parser.add_argument('--num', type=int, searchable=True)
169
+ >>> parser.add_argument('--other-num', type=int, searchable=True, default="args.num")
170
+ >>> parser.parse_args("--num 1 2".split())
171
+ [Namespace(num=1, other_num=1), Namespace(num=2, other_num=2)]
172
+ ```
173
+
174
+ You can also specify so in the command line, i.e., `args.<name-of-other-argument>` does not have to appear in the default value of the argument.
175
+
176
+ This allows you the flexibility to have a parameter default to another parameter's values, and then specify different values when need arises (example use case: specify different CUDA device for a specific component only when OOM errors are encountered, and have it default to the "general" device otherwise).
177
+
178
+ ### Different value for each dataset split
179
+
180
+ You can specify the kw argument `splits` to create one argument per split:
181
+
182
+ ```python
183
+ >>> parser = gridparse.GridArgumentParser()
184
+ >>> parser.add_argument('--num', type=int, searchable=True)
185
+ >>> parser.add_argument('--other-num', type=int, splits=["train", "test"])
186
+ >>> parser.parse_args("--num 1 2 --train-other-num 3 --test-other-num 5".split())
187
+ [Namespace(num=1, test_other_num=5, train_other_num=3), Namespace(num=2, test_other_num=5, train_other_num=3)]
188
+ ```
189
+
190
+ Note that if an underscore (`_`) exists in the name of the argument, the new names will also join the splits with the original name with an underscore: `--other_num` to `--train_other_num`, etc. The new arguments are separate, i.e. if searchable, you do *not* have to specify the same number of values, etc. They each gain all the properties specified in the original argument.
@@ -0,0 +1,138 @@
1
+ # GridParse
2
+
3
+ A lightweight (only dependency is `omegaconf` which also downloads `yaml`) `ArgumentParser` --- aka `GridArgumentParser` --- that supports your *grid-search* needs. Supports top-level parser and subparsers. Configuration files of any type (using `omegaconf`) as also support through the argument `--gridparse-config` (also available with underscore `_`), where multiple configuration files can be passed and parsed.
4
+
5
+ ## Overview
6
+
7
+ It transforms the following arguments in the corresponding way:
8
+
9
+ `--arg 1` &rarr; `--arg 1 2 3`
10
+
11
+ `--arg 1 2 3` &rarr; `--arg 1~~2~~3 4~~5~~6`
12
+
13
+ `--arg 1-2-3 4-5-6` &rarr; `--arg 1-2-3~~4-5-6 7-8-9~~10-11`
14
+
15
+ So, for single arguments, it extends them similar to nargs="+". For multiple arguments, it extends them with `list_as_dashed_str(type, delimiter="~~")` (available in `gridparse.utils`), and this is recursively applied with existing `list_as_dashed_str` types. It can also handle subspaces using square brackets, where you can enclose combinations of hyperparameters within but don't have them combine with values of hyperparameters in other subspaces of the same length.
16
+
17
+ *Note*: when using at least on searchable argument, the return value of `parse_args()` is always a list of Namespaces, otherwise it is just a Namespace.
18
+
19
+ ## Examples
20
+
21
+ Example without subspaces:
22
+
23
+ ```python
24
+ parser = GridArgumentParser()
25
+ parser.add_argument("--hparam1", type=int, searchable=True)
26
+ parser.add_argument("--hparam2", nargs="+", type=int, searchable=True)
27
+ parser.add_argument("--normal", required=True, type=str)
28
+ parser.add_argument(
29
+ "--lists",
30
+ required=True,
31
+ nargs="+",
32
+ type=list_as_dashed_str(str),
33
+ searchable=True,
34
+ )
35
+ parser.add_argument(
36
+ "--normal_lists",
37
+ required=True,
38
+ nargs="+",
39
+ type=list_as_dashed_str(str),
40
+ )
41
+ args = parser.parse_args(
42
+ (
43
+ "--hparam1 1~~2~~3 --hparam2 4~~3 5~~4 6~~5 "
44
+ "--normal efrgthytfgn --lists 1-2-3 3-4-5~~6-7 "
45
+ "--normal_lists 1-2-3 4-5-6"
46
+ ).split()
47
+ )
48
+ assert len(args) == 1 * 3 * 1 * 2 * 1 # corresponding number of different values in input CL arguments
49
+
50
+ pprint(args)
51
+ ```
52
+
53
+ Output:
54
+
55
+ ```python
56
+ [
57
+
58
+ Namespace(hparam1=[1, 2, 3], hparam2=[4, 3], lists=[['1', '2', '3']], normal='efrgthytfgn', normal_lists=[['1', '2', '3'], ['4', '5', '6']]),
59
+
60
+ Namespace(hparam1=[1, 2, 3], hparam2=[5, 4], lists=[['1', '2', '3']], normal='efrgthytfgn', normal_lists=[['1', '2', '3'], ['4', '5', '6']]),
61
+
62
+ Namespace(hparam1=[1, 2, 3], hparam2=[6, 5], lists=[['1', '2', '3']], normal='efrgthytfgn', normal_lists=[['1', '2', '3'], ['4', '5', '6']]),
63
+
64
+ Namespace(hparam1=[1, 2, 3], hparam2=[4, 3], lists=[['3', '4', '5'], ['6', '7']], normal='efrgthytfgn', normal_lists=[['1', '2', '3'], ['4', '5', '6']]),
65
+
66
+ Namespace(hparam1=[1, 2, 3], hparam2=[5, 4], lists=[['3', '4', '5'], ['6', '7']], normal='efrgthytfgn', normal_lists=[['1', '2', '3'], ['4', '5', '6']]),
67
+
68
+ Namespace(hparam1=[1, 2, 3], hparam2=[6, 5], lists=[['3', '4', '5'], ['6', '7']], normal='efrgthytfgn', normal_lists=[['1', '2', '3'], ['4', '5', '6']])
69
+
70
+ ]
71
+ ```
72
+
73
+ Example with subspaces:
74
+
75
+ ```python
76
+ parser = GridArgumentParser()
77
+ parser.add_argument("--hparam1", type=int, searchable=True)
78
+ parser.add_argument("--hparam2", type=int, searchable=True)
79
+ parser.add_argument("--hparam3", type=int, searchable=True, default=1000)
80
+ parser.add_argument("--hparam4", type=int, searchable=True, default=2000)
81
+ parser.add_argument("--normal", required=True, type=str)
82
+
83
+ args = parser.parse_args(
84
+ (
85
+ "--hparam1 1 2 "
86
+ "{--hparam2 1 2 3 {--normal normal --hparam4 100 101 102} {--normal maybe --hparam4 200 201 202 203}} "
87
+ "{--hparam2 4 5 6 --normal not-normal}"
88
+ ).split()
89
+ )
90
+ assert len(args) == 2 * ((3 * (3 + 4)) + 3)
91
+ ```
92
+
93
+ ## Additional capabilities
94
+
95
+ ### Configuration files
96
+
97
+ Using `omegaconf` (the only dependency), we allow users to specify (potentially multiple) configuration files that can be used to populate the resulting namespace(s). Access the through the `gridparse-config` argument: `--gridparse-config /this/config.json /that/config.yml`. Command-line arguments are given higher priority, and then the priority is in order of appearance in the command line for the configuration files.
98
+
99
+ ### Specify `None` in command-line
100
+
101
+ In case some parameter is searchable (and not a boolean), you might need one of the values to be the default value `None`. In that case, specifying any other value would rule the value `None` out from the grid search. To avoid this, `gridparse` allows you to specify the value `_None_` in the command line:
102
+
103
+ ```python
104
+ >>> parser = gridparse.GridArgumentParser()
105
+ >>> parser.add_argument('--text', type=str, searchable=True)
106
+ >>> parser.parse_args("--text a b _None_".split())
107
+ [Namespace(text='a'), Namespace(text='b'), Namespace(text=None)]
108
+ ```
109
+
110
+ ### Access values of other parameter
111
+
112
+ Moreover, you can use the value (not the default) of another argument as the default by setting the default to `args.<name-of-other-argument>`.
113
+
114
+ ```python
115
+ >>> parser = gridparse.GridArgumentParser()
116
+ >>> parser.add_argument('--num', type=int, searchable=True)
117
+ >>> parser.add_argument('--other-num', type=int, searchable=True, default="args.num")
118
+ >>> parser.parse_args("--num 1 2".split())
119
+ [Namespace(num=1, other_num=1), Namespace(num=2, other_num=2)]
120
+ ```
121
+
122
+ You can also specify so in the command line, i.e., `args.<name-of-other-argument>` does not have to appear in the default value of the argument.
123
+
124
+ This allows you the flexibility to have a parameter default to another parameter's values, and then specify different values when need arises (example use case: specify different CUDA device for a specific component only when OOM errors are encountered, and have it default to the "general" device otherwise).
125
+
126
+ ### Different value for each dataset split
127
+
128
+ You can specify the kw argument `splits` to create one argument per split:
129
+
130
+ ```python
131
+ >>> parser = gridparse.GridArgumentParser()
132
+ >>> parser.add_argument('--num', type=int, searchable=True)
133
+ >>> parser.add_argument('--other-num', type=int, splits=["train", "test"])
134
+ >>> parser.parse_args("--num 1 2 --train-other-num 3 --test-other-num 5".split())
135
+ [Namespace(num=1, test_other_num=5, train_other_num=3), Namespace(num=2, test_other_num=5, train_other_num=3)]
136
+ ```
137
+
138
+ Note that if an underscore (`_`) exists in the name of the argument, the new names will also join the splits with the original name with an underscore: `--other_num` to `--train_other_num`, etc. The new arguments are separate, i.e. if searchable, you do *not* have to specify the same number of values, etc. They each gain all the properties specified in the original argument.
@@ -0,0 +1,4 @@
1
+ from argparse import *
2
+
3
+ from .grid_argument_parser import GridArgumentParser
4
+ from .utils import list_as_dashed_str, strbool
@@ -0,0 +1,539 @@
1
+ import os
2
+ import argparse
3
+ import warnings
4
+ from typing import Any, Tuple, List, Optional, Union, Sequence
5
+ from copy import deepcopy
6
+ from omegaconf import OmegaConf
7
+
8
+ from gridparse.utils import list_as_dashed_str, strbool
9
+
10
+
11
+ # overwritten to fix issue in __call__
12
+ class _GridSubparsersAction(argparse._SubParsersAction):
13
+ def __call__(
14
+ self,
15
+ parser: argparse.ArgumentParser,
16
+ namespace: argparse.Namespace, # contains only subparser arg
17
+ values: Optional[
18
+ Union[str, Sequence[Any]]
19
+ ], # contains all args for gridparse
20
+ option_string: Optional[str] = None,
21
+ ) -> None:
22
+ parser_name = values[0]
23
+ arg_strings = values[1:]
24
+
25
+ # set the parser name if requested
26
+ if self.dest is not argparse.SUPPRESS:
27
+ setattr(namespace, self.dest, parser_name)
28
+
29
+ # select the parser
30
+ try:
31
+ parser = self._name_parser_map[parser_name]
32
+ except KeyError:
33
+ args = {
34
+ 'parser_name': parser_name,
35
+ 'choices': ', '.join(self._name_parser_map),
36
+ }
37
+ msg = (
38
+ argparse._(
39
+ 'unknown parser %(parser_name)r (choices: %(choices)s)'
40
+ )
41
+ % args
42
+ )
43
+ raise argparse.ArgumentError(self, msg)
44
+
45
+ # parse all the remaining options into the namespace
46
+ # store any unrecognized options on the object, so that the top
47
+ # level parser can decide what to do with them
48
+
49
+ # In case this subparser defines new defaults, we parse them
50
+ # in a new namespace object and then update the original
51
+ # namespace for the relevant parts.
52
+ # NOTE: changed here because parser.parse_args() now returns a list
53
+ # of namespaces instead of a single namespace
54
+
55
+ namespaces = []
56
+
57
+ subnamespaces, arg_strings = parser.parse_known_args(arg_strings, None)
58
+ for subnamespace in subnamespaces:
59
+ new_namespace = deepcopy(namespace)
60
+ for key, value in vars(subnamespace).items():
61
+ setattr(new_namespace, key, value)
62
+ namespaces.append(new_namespace)
63
+
64
+ if arg_strings:
65
+ for ns in namespaces:
66
+ vars(ns).setdefault(argparse._UNRECOGNIZED_ARGS_ATTR, [])
67
+ getattr(ns, argparse._UNRECOGNIZED_ARGS_ATTR).extend(
68
+ arg_strings
69
+ )
70
+
71
+ # hacky way to return all namespaces in subparser
72
+ # method is supposed to perform in-place modification
73
+ # of namespace, so we add a new attribute
74
+ namespace.___namespaces___ = namespaces
75
+
76
+
77
+ # overwritten to include our _SubparserAction
78
+ class _GridActionsContainer(argparse._ActionsContainer):
79
+ def __init__(self, *args, **kwargs):
80
+ super().__init__(*args, **kwargs)
81
+ self.register("action", "parsers", _GridSubparsersAction)
82
+
83
+
84
+ class GridArgumentParser(_GridActionsContainer, argparse.ArgumentParser):
85
+ """ArgumentParser that supports grid search.
86
+
87
+ It transforms the following arguments in the corresponding way:
88
+
89
+ --arg 1 -> --arg 1 2 3
90
+
91
+ --arg 1 2 3 -> --arg 1~~2~~3 4~~5~~6
92
+
93
+ --arg 1-2-3 4-5-6 -> --arg 1-2-3~~4-5-6 7-8-9~~10-11
94
+
95
+ So, for single arguments, it extends them similar to nargs="+".
96
+ For multiple arguments, it extends them with
97
+ list_as_dashed_str(type, delimiter="~~"), and this is recursively
98
+ applied with existing list_as_dashed_str types. It can also handle subspaces
99
+ using square brackets, where you can enclose combinations of hyperparameters
100
+ within but don't have them combine with values of hyperparameters in other
101
+ subspaces of the same length.
102
+
103
+ Example without subspaces:
104
+ ```
105
+ parser = GridArgumentParser()
106
+ parser.add_argument("--hparam1", type=int, searchable=True)
107
+ parser.add_argument("--hparam2", nargs="+", type=int, searchable=True)
108
+ parser.add_argument("--normal", required=True, type=str)
109
+ parser.add_argument(
110
+ "--lists",
111
+ required=True,
112
+ nargs="+",
113
+ type=list_as_dashed_str(str),
114
+ searchable=True,
115
+ )
116
+ parser.add_argument(
117
+ "--normal_lists",
118
+ required=True,
119
+ nargs="+",
120
+ type=list_as_dashed_str(str),
121
+ )
122
+ args = parser.parse_args(
123
+ (
124
+ "--hparam1 1~~2~~3 --hparam2 4~~3 5~~4 6~~5 "
125
+ "--normal efrgthytfgn --lists 1-2-3 3-4-5~~6-7 "
126
+ "--normal_lists 1-2-3 4-5-6"
127
+ ).split()
128
+ )
129
+ assert len(args) == 1 * 3 * 1 * 2 * 1 # corresponding number of different values in input CL arguments
130
+
131
+ pprint(args)
132
+ ```
133
+
134
+ Output:
135
+ ```
136
+ [Namespace(hparam1=[1, 2, 3], hparam2=[4, 3], lists=[['1', '2', '3']], normal='efrgthytfgn', normal_lists=[['1', '2', '3'], ['4', '5', '6']]),
137
+ Namespace(hparam1=[1, 2, 3], hparam2=[5, 4], lists=[['1', '2', '3']], normal='efrgthytfgn', normal_lists=[['1', '2', '3'], ['4', '5', '6']]),
138
+ Namespace(hparam1=[1, 2, 3], hparam2=[6, 5], lists=[['1', '2', '3']], normal='efrgthytfgn', normal_lists=[['1', '2', '3'], ['4', '5', '6']]),
139
+ Namespace(hparam1=[1, 2, 3], hparam2=[4, 3], lists=[['3', '4', '5'], ['6', '7']], normal='efrgthytfgn', normal_lists=[['1', '2', '3'], ['4', '5', '6']]),
140
+ Namespace(hparam1=[1, 2, 3], hparam2=[5, 4], lists=[['3', '4', '5'], ['6', '7']], normal='efrgthytfgn', normal_lists=[['1', '2', '3'], ['4', '5', '6']]),
141
+ Namespace(hparam1=[1, 2, 3], hparam2=[6, 5], lists=[['3', '4', '5'], ['6', '7']], normal='efrgthytfgn', normal_lists=[['1', '2', '3'], ['4', '5', '6']])]
142
+ ```
143
+
144
+ Example with subspaces:
145
+ ```
146
+ parser = GridArgumentParser()
147
+ parser.add_argument("--hparam1", type=int, searchable=True)
148
+ parser.add_argument("--hparam2", type=int, searchable=True)
149
+ parser.add_argument("--hparam3", type=int, searchable=True, default=1000)
150
+ parser.add_argument("--hparam4", type=int, searchable=True, default=2000)
151
+ parser.add_argument("--normal", required=True, type=str)
152
+
153
+ args = parser.parse_args(
154
+ (
155
+ "--hparam1 1 2 "
156
+ "{--hparam2 1 2 3 {--normal normal --hparam4 100 101 102} {--normal maybe --hparam4 200 201 202 203}} "
157
+ "{--hparam2 4 5 6 --normal not-normal}"
158
+ ).split()
159
+ )
160
+ assert len(args) == 2 * ((3 * (3 + 4)) + 3)
161
+ ```
162
+ """
163
+
164
+ def __init__(self, retain_config_filename: bool = False, *args, **kwargs):
165
+ """Initializes the GridArgumentParser.
166
+
167
+ Args:
168
+ retain_config_filename: whether to keep the `gridparse-config` argument
169
+ in the namespace or not.
170
+ """
171
+ self._grid_args = []
172
+ self._retain_config_filename = retain_config_filename
173
+ super().__init__(*args, **kwargs)
174
+ self.add_argument(
175
+ "--gridparse-config",
176
+ "--gridparse_config",
177
+ type=str,
178
+ nargs="*",
179
+ help="Path to a configuration file with default values for parser. "
180
+ "Values will be used if not provided in the command line.",
181
+ )
182
+
183
+
184
+ def parse_args(self, *args, **kwargs):
185
+ vals = super().parse_args(*args, **kwargs)
186
+ # hacky way to return namespaces in subparser
187
+ if "___namespaces___" in vals[0]:
188
+ vals = [ns for subps_ns in vals for ns in subps_ns.___namespaces___]
189
+
190
+ # get unrecognized arguments from other namespaces
191
+ if hasattr(vals[0], argparse._UNRECOGNIZED_ARGS_ATTR):
192
+ argv = getattr(vals[0], argparse._UNRECOGNIZED_ARGS_ATTR)
193
+ msg = argparse._("unrecognized arguments: %s")
194
+ self.error(msg % " ".join(argv))
195
+
196
+ for ns in vals:
197
+ # get defaults from other arguments
198
+ for arg in dir(ns):
199
+ val = getattr(ns, arg)
200
+ if isinstance(val, str) and val.startswith("args."):
201
+ borrow_arg = val.split("args.")[1]
202
+ setattr(ns, arg, getattr(ns, borrow_arg, None))
203
+
204
+ # is_grid_search = len(self._grid_args) > 0
205
+ # for potential_subparser in getattr(
206
+ # self._subparsers, "_group_actions", []
207
+ # ):
208
+ # try:
209
+ # grid_args = next(
210
+ # iter(potential_subparser.choices.values())
211
+ # )._grid_args
212
+ # is_grid_search = is_grid_search or grid_args
213
+ # except AttributeError:
214
+ # continue
215
+ # if len(vals) == 1 and not is_grid_search:
216
+ # warnings.warn("Use")
217
+ # return vals[0]
218
+
219
+ for ns in vals:
220
+ cfg = {}
221
+ if ns.gridparse_config is not None:
222
+ # reverse for priority to originally first configs
223
+ for potential_fn in reversed(getattr(ns, "gridparse_config", [])):
224
+ if os.path.isfile(potential_fn):
225
+ cfg = OmegaConf.merge(cfg, OmegaConf.load(potential_fn))
226
+
227
+ for arg in cfg:
228
+ if not hasattr(ns, arg):
229
+ continue
230
+ if getattr(ns, arg) is None:
231
+ setattr(ns, arg, getattr(cfg, arg))
232
+
233
+ if not self._retain_config_filename:
234
+ delattr(ns, "gridparse_config")
235
+
236
+ return vals
237
+
238
+ def _check_value(self, action, value):
239
+ """Overwrites `_check_value` to support grid search with `None`s."""
240
+ # converted value must be one of the choices (if specified)
241
+ if action.choices is not None and (
242
+ value not in action.choices and value is not None
243
+ ): # allows value to be None without error
244
+ args = {
245
+ "value": value,
246
+ "choices": ", ".join(map(repr, action.choices))
247
+ }
248
+ msg = argparse._(
249
+ "invalid choice: %(value)r (choose from %(choices)s)"
250
+ )
251
+ raise argparse.ArgumentError(action, msg % args)
252
+
253
+ def _get_value(self, action, arg_string):
254
+ """Overwrites `_get_value` to support grid search.
255
+ It is used to parse the value of an argument.
256
+ """
257
+ type_func = self._registry_get('type', action.type, action.type)
258
+ default = action.default
259
+
260
+ # if default is "args.X" value,
261
+ # then set up value so that X is grabbed from the same namespace later
262
+ if (isinstance(default, str) and default.startswith("args.")) and (
263
+ default == arg_string or arg_string is None
264
+ ):
265
+ return default
266
+
267
+ # if arg_string is "args.X" value,
268
+ # then set up value so that X is grabbed from the same namespace later
269
+ if arg_string.startswith("args."):
270
+ return arg_string
271
+
272
+ # if arg_string is "_None_", then return None
273
+ if (
274
+ arg_string == "_None_"
275
+ and action.dest in self._grid_args
276
+ and action.type is not strbool
277
+ ):
278
+ return None
279
+
280
+ if not callable(type_func):
281
+ msg = argparse._('%r is not callable')
282
+ raise argparse.ArgumentError(action, msg % type_func)
283
+
284
+ # convert the value to the appropriate type
285
+ try:
286
+ result = type_func(arg_string)
287
+
288
+ # ArgumentTypeErrors indicate errors
289
+ except argparse.ArgumentTypeError:
290
+ name = getattr(action.type, '__name__', repr(action.type))
291
+ msg = str(argparse._sys.exc_info()[1])
292
+ raise argparse.ArgumentError(action, msg)
293
+
294
+ # TypeErrors or ValueErrors also indicate errors
295
+ except (TypeError, ValueError):
296
+ name = getattr(action.type, '__name__', repr(action.type))
297
+ args = {'type': name, 'value': arg_string}
298
+ msg = argparse._('invalid %(type)s value: %(value)r')
299
+ raise argparse.ArgumentError(action, msg % args)
300
+
301
+ # return the converted value
302
+ return result
303
+
304
+ @staticmethod
305
+ def _add_split_in_arg(arg: str, split: str) -> str:
306
+ """Adds the `split` to the name of the argument `arg`."""
307
+
308
+ if "_" in arg:
309
+ # if the user uses "_" as a delimiter, we use that
310
+ delim = "_"
311
+ else:
312
+ # otherwise, we use "-" (no necessary to check for it, e.g., could be CamelCase)
313
+ delim = "-"
314
+ return split + delim + arg
315
+
316
+ def add_argument(self, *args, **kwargs) -> Union[argparse.Action, List[argparse.Action]]:
317
+ """Augments `add_argument` to support grid search.
318
+ For parameters that are searchable, provide specification
319
+ for a single value, and set the new argument `searchable`
320
+ to `True`.
321
+ """
322
+
323
+ ## copy-pasted code
324
+ chars = self.prefix_chars
325
+ if not args or (len(args) == 1 and args[0][0] not in chars):
326
+ if args and "dest" in kwargs:
327
+ raise ValueError("dest supplied twice for positional argument")
328
+ new_kwargs = self._get_positional_kwargs(*args, **kwargs)
329
+
330
+ # otherwise, we're adding an optional argument
331
+ else:
332
+ new_kwargs = self._get_optional_kwargs(*args, **kwargs)
333
+ ## edoc detsap-ypoc
334
+
335
+ # create multiple arguments for each split
336
+ splits = kwargs.pop("splits", [])
337
+ if splits:
338
+ actions = []
339
+ for split in splits:
340
+
341
+ cp_args = deepcopy(list(args))
342
+ cp_kwargs = deepcopy(kwargs)
343
+
344
+ if args:
345
+ i = 0
346
+ while cp_args[0][i] in self.prefix_chars:
347
+ i += 1
348
+
349
+ cp_args[0] = cp_args[0][:i] + self._add_split_in_arg(cp_args[0][i:], split)
350
+
351
+ else:
352
+ cp_kwargs["dest"] = self._add_split_in_arg(cp_kwargs["dest"], split)
353
+
354
+ actions.append(self.add_argument(*cp_args, **cp_kwargs))
355
+
356
+ return actions
357
+
358
+ type = kwargs.get("type", None)
359
+ if type is not None and type == bool:
360
+ kwargs["type"] = strbool
361
+
362
+ type = kwargs.get("type", None)
363
+ if type is not None and type == strbool:
364
+ kwargs.setdefault("default", "false")
365
+
366
+ searchable = kwargs.pop("searchable", False)
367
+ if searchable:
368
+ dest = new_kwargs["dest"]
369
+ self._grid_args.append(dest)
370
+
371
+ nargs = kwargs.get("nargs", None)
372
+ type = kwargs.get("type", None)
373
+
374
+ if nargs == "+":
375
+ type = list_as_dashed_str(type, delimiter="~~")
376
+ else:
377
+ nargs = "+"
378
+
379
+ kwargs["nargs"] = nargs
380
+ kwargs["type"] = type
381
+
382
+ # doesn't add `searchable` in _StoreAction
383
+ return super().add_argument(*args, **kwargs)
384
+
385
+ class Subspace:
386
+ def __init__(self, parent: Optional["Subspace"] = None):
387
+ self.args = {}
388
+ self.subspaces = {}
389
+ self.cnt = 0
390
+ self.parent = parent
391
+
392
+ def add_arg(self, arg: str):
393
+ if arg == "{":
394
+ new_subspace = GridArgumentParser.Subspace(self)
395
+ self.subspaces[self.cnt] = new_subspace
396
+ self.cnt += 1
397
+ return new_subspace
398
+ elif arg == "}":
399
+ return self.parent
400
+ else:
401
+ self.args[self.cnt] = arg
402
+ self.cnt += 1
403
+ return self
404
+
405
+ def parse_paths(self) -> List[List[str]]:
406
+
407
+ if not self.subspaces:
408
+ return [list(self.args.values())]
409
+
410
+ this_subspace_args = []
411
+ cumulative_args = []
412
+
413
+ for i in range(self.cnt):
414
+ if i in self.subspaces:
415
+ paths = self.subspaces[i].parse_paths()
416
+ for path in paths:
417
+ cumulative_args.append(this_subspace_args + path)
418
+ else:
419
+ this_subspace_args.append(self.args[i])
420
+ for path in cumulative_args:
421
+ path.append(self.args[i])
422
+
423
+ return cumulative_args
424
+
425
+ def __repr__(self) -> str:
426
+ repr = "Subspace("
427
+ for i in range(self.cnt):
428
+ if i in self.subspaces:
429
+ repr += f"{self.subspaces[i]}, "
430
+ else:
431
+ repr += f"{self.args[i]}, "
432
+ repr = repr[:-2] + ")"
433
+ return repr
434
+
435
+ def _parse_known_args(
436
+ self, arg_strings: List[str], namespace: argparse.Namespace
437
+ ) -> Tuple[List[argparse.Namespace], List[str]]:
438
+ """Augments `_parse_known_args` to support grid search.
439
+ Different values for the same argument are expanded into
440
+ multiple namespaces.
441
+
442
+ Returns:
443
+ A list of namespaces instead os a single namespace.
444
+ """
445
+
446
+ # if { and } denote a subspace and not inside a string of something else
447
+ new_arg_strings = []
448
+ for arg in arg_strings:
449
+ new_args = [None, arg, None]
450
+
451
+ # find leftmost { and rightmost }
452
+ idx_ocb = arg.find("{")
453
+ idx_ccb = arg.rfind("}")
454
+
455
+ cnt = 0
456
+ for i in range(len(arg)):
457
+ if arg[i] == "{":
458
+ cnt += 1
459
+ elif arg[i] == "}":
460
+ cnt -= 1
461
+
462
+ # if arg starts with { and end with }, doesn't have a },
463
+ # or has at least an extra {, then it's a subspace
464
+ if idx_ocb == 0 and (idx_ccb in (len(arg) - 1, -1) or cnt > 0):
465
+ new_args[0] = "{"
466
+ new_args[1] = new_args[1][1:]
467
+ elif idx_ocb == 0 and cnt <= 0:
468
+ warnings.warn(
469
+ "Found { at the beginning and some } in the middle "
470
+ f"of the argument: `{arg}`."
471
+ " This is not considered a \{\} subspace."
472
+ )
473
+ # if arg ends with } and doesn't have a {, starts with {,
474
+ # or has at least an extra }, then it's a subspace
475
+ if idx_ccb == len(arg) - 1 and (idx_ocb in (0, -1) or cnt < 0):
476
+ new_args[1] = new_args[1][:-1]
477
+ new_args[2] = "}"
478
+ elif idx_ccb == len(arg) - 1 and cnt >= 0:
479
+ warnings.warn(
480
+ "Found } at the end and some { in the middle "
481
+ f"of argument: `{arg}`."
482
+ " This is not considered a \{\} subspace."
483
+ )
484
+
485
+ new_arg_strings.extend([a for a in new_args if a])
486
+
487
+ arg_strings = new_arg_strings
488
+
489
+ # break arg_strings into subspaces on { and }
490
+ root_subspace = self.Subspace()
491
+ current_subspace = root_subspace
492
+
493
+ for arg in arg_strings:
494
+ current_subspace = current_subspace.add_arg(arg)
495
+
496
+ all_arg_strings = root_subspace.parse_paths()
497
+ all_namespaces = []
498
+ all_args = []
499
+
500
+ if not all_arg_strings:
501
+ namespace, args = super()._parse_known_args(
502
+ arg_strings, deepcopy(namespace)
503
+ )
504
+ return [namespace], args
505
+
506
+ # for all possible combinations in the grid search subspaces
507
+ for arg_strings in all_arg_strings:
508
+ new_namespace, args = super()._parse_known_args(
509
+ arg_strings, deepcopy(namespace)
510
+ )
511
+
512
+ namespaces = [deepcopy(new_namespace)]
513
+
514
+ for arg in self._grid_args:
515
+ if not hasattr(new_namespace, arg):
516
+ continue
517
+ values = getattr(new_namespace, arg)
518
+ for ns in namespaces:
519
+ ns.__delattr__(arg)
520
+ if not isinstance(values, list):
521
+ values = [values]
522
+
523
+ # duplicate the existing namespaces
524
+ # for all different values of the grid search param
525
+
526
+ new_namespaces = []
527
+
528
+ for value in values:
529
+ for ns in namespaces:
530
+ new_ns = deepcopy(ns)
531
+ setattr(new_ns, arg, value)
532
+ new_namespaces.append(new_ns)
533
+
534
+ namespaces = new_namespaces
535
+
536
+ all_namespaces.extend(namespaces)
537
+ all_args.extend(args)
538
+
539
+ return all_namespaces, all_args
@@ -0,0 +1,52 @@
1
+ import argparse
2
+ from typing import Callable
3
+
4
+
5
+ # TODO: has issues with negative numbers
6
+ def list_as_dashed_str(actual_type: Callable, delimiter: str = "-"):
7
+ """Creates a function that converts a string to a list of elements.
8
+
9
+ Can be used as the `type` argument in `argparse` to convert
10
+ a string to a list of elements, e.g.: `["1-2-3", "4-5-6"]` to
11
+ `[[1, 2, 3], [4, 5, 6]]` (note that this operates on a single
12
+ `str` at a time, use `nargs` to pass multiple).
13
+
14
+ Args:
15
+ actual_type: the type of the elements in the list
16
+ (or any function that takes a `str` and returns something).
17
+ delimiter: the delimiter between elements in `str`.
18
+ """
19
+
20
+ def _list_of_lists(s: str):
21
+ if s == "None":
22
+ return None
23
+ l = [actual_type(e) for e in s.split(delimiter)]
24
+ return l
25
+
26
+ return _list_of_lists
27
+
28
+
29
+ def strbool(arg: str):
30
+ """Converts a string boolean to an actual boolean.
31
+
32
+ This is useful for searching over boolean hyperparameters,
33
+ because now multiple values can be passed with `searchable=True`:
34
+ "--flag true false".
35
+
36
+ Args:
37
+ arg: the string to convert.
38
+
39
+ Raises:
40
+ argparse.ArgumentTypeError: if the string is not a valid boolean.
41
+ """
42
+ if isinstance(arg, bool):
43
+ return arg
44
+
45
+ if arg is None:
46
+ return False
47
+
48
+ if arg.lower() == 'true':
49
+ return True
50
+ if arg.lower() == 'false':
51
+ return False
52
+ raise argparse.ArgumentTypeError('Boolean value expected.')
@@ -0,0 +1,190 @@
1
+ Metadata-Version: 2.2
2
+ Name: gridparse
3
+ Version: 1.5.0
4
+ Summary: Grid search directly from argparse
5
+ Home-page: https://github.com/gchochla/gridparse
6
+ Author: Georgios Chochlakis
7
+ Author-email: "Georgios (Yiorgos) Chochlakis" <georgioschochlakis@gmail.com>
8
+ Maintainer-email: "Georgios (Yiorgos) Chochlakis" <georgioschochlakis@gmail.com>
9
+ License: MIT License
10
+
11
+ Copyright (c) 2023 Georgios Chochlakis
12
+
13
+ Permission is hereby granted, free of charge, to any person obtaining a copy
14
+ of this software and associated documentation files (the "Software"), to deal
15
+ in the Software without restriction, including without limitation the rights
16
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17
+ copies of the Software, and to permit persons to whom the Software is
18
+ furnished to do so, subject to the following conditions:
19
+
20
+ The above copyright notice and this permission notice shall be included in all
21
+ copies or substantial portions of the Software.
22
+
23
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
29
+ SOFTWARE.
30
+ Project-URL: Homepage, https://github.com/gchochla/gridparse
31
+ Project-URL: Bug Reports, https://github.com/gchochla/gridparse/issues
32
+ Project-URL: Source, https://github.com/gchochla/gridparse
33
+ Keywords: machine learning,grid search,argparse
34
+ Classifier: Development Status :: 3 - Alpha
35
+ Classifier: License :: OSI Approved :: MIT License
36
+ Classifier: Programming Language :: Python :: 3
37
+ Classifier: Programming Language :: Python :: 3.7
38
+ Classifier: Programming Language :: Python :: 3.8
39
+ Classifier: Programming Language :: Python :: 3.9
40
+ Classifier: Programming Language :: Python :: 3.10
41
+ Classifier: Programming Language :: Python :: 3.11
42
+ Classifier: Programming Language :: Python :: 3 :: Only
43
+ Requires-Python: >=3.7
44
+ Description-Content-Type: text/markdown
45
+ License-File: LICENSE
46
+ Requires-Dist: omegaconf
47
+ Provides-Extra: dev
48
+ Requires-Dist: black; extra == "dev"
49
+ Requires-Dist: pytest; extra == "dev"
50
+ Dynamic: author
51
+ Dynamic: home-page
52
+
53
+ # GridParse
54
+
55
+ A lightweight (only dependency is `omegaconf` which also downloads `yaml`) `ArgumentParser` --- aka `GridArgumentParser` --- that supports your *grid-search* needs. Supports top-level parser and subparsers. Configuration files of any type (using `omegaconf`) as also support through the argument `--gridparse-config` (also available with underscore `_`), where multiple configuration files can be passed and parsed.
56
+
57
+ ## Overview
58
+
59
+ It transforms the following arguments in the corresponding way:
60
+
61
+ `--arg 1` &rarr; `--arg 1 2 3`
62
+
63
+ `--arg 1 2 3` &rarr; `--arg 1~~2~~3 4~~5~~6`
64
+
65
+ `--arg 1-2-3 4-5-6` &rarr; `--arg 1-2-3~~4-5-6 7-8-9~~10-11`
66
+
67
+ So, for single arguments, it extends them similar to nargs="+". For multiple arguments, it extends them with `list_as_dashed_str(type, delimiter="~~")` (available in `gridparse.utils`), and this is recursively applied with existing `list_as_dashed_str` types. It can also handle subspaces using square brackets, where you can enclose combinations of hyperparameters within but don't have them combine with values of hyperparameters in other subspaces of the same length.
68
+
69
+ *Note*: when using at least on searchable argument, the return value of `parse_args()` is always a list of Namespaces, otherwise it is just a Namespace.
70
+
71
+ ## Examples
72
+
73
+ Example without subspaces:
74
+
75
+ ```python
76
+ parser = GridArgumentParser()
77
+ parser.add_argument("--hparam1", type=int, searchable=True)
78
+ parser.add_argument("--hparam2", nargs="+", type=int, searchable=True)
79
+ parser.add_argument("--normal", required=True, type=str)
80
+ parser.add_argument(
81
+ "--lists",
82
+ required=True,
83
+ nargs="+",
84
+ type=list_as_dashed_str(str),
85
+ searchable=True,
86
+ )
87
+ parser.add_argument(
88
+ "--normal_lists",
89
+ required=True,
90
+ nargs="+",
91
+ type=list_as_dashed_str(str),
92
+ )
93
+ args = parser.parse_args(
94
+ (
95
+ "--hparam1 1~~2~~3 --hparam2 4~~3 5~~4 6~~5 "
96
+ "--normal efrgthytfgn --lists 1-2-3 3-4-5~~6-7 "
97
+ "--normal_lists 1-2-3 4-5-6"
98
+ ).split()
99
+ )
100
+ assert len(args) == 1 * 3 * 1 * 2 * 1 # corresponding number of different values in input CL arguments
101
+
102
+ pprint(args)
103
+ ```
104
+
105
+ Output:
106
+
107
+ ```python
108
+ [
109
+
110
+ Namespace(hparam1=[1, 2, 3], hparam2=[4, 3], lists=[['1', '2', '3']], normal='efrgthytfgn', normal_lists=[['1', '2', '3'], ['4', '5', '6']]),
111
+
112
+ Namespace(hparam1=[1, 2, 3], hparam2=[5, 4], lists=[['1', '2', '3']], normal='efrgthytfgn', normal_lists=[['1', '2', '3'], ['4', '5', '6']]),
113
+
114
+ Namespace(hparam1=[1, 2, 3], hparam2=[6, 5], lists=[['1', '2', '3']], normal='efrgthytfgn', normal_lists=[['1', '2', '3'], ['4', '5', '6']]),
115
+
116
+ Namespace(hparam1=[1, 2, 3], hparam2=[4, 3], lists=[['3', '4', '5'], ['6', '7']], normal='efrgthytfgn', normal_lists=[['1', '2', '3'], ['4', '5', '6']]),
117
+
118
+ Namespace(hparam1=[1, 2, 3], hparam2=[5, 4], lists=[['3', '4', '5'], ['6', '7']], normal='efrgthytfgn', normal_lists=[['1', '2', '3'], ['4', '5', '6']]),
119
+
120
+ Namespace(hparam1=[1, 2, 3], hparam2=[6, 5], lists=[['3', '4', '5'], ['6', '7']], normal='efrgthytfgn', normal_lists=[['1', '2', '3'], ['4', '5', '6']])
121
+
122
+ ]
123
+ ```
124
+
125
+ Example with subspaces:
126
+
127
+ ```python
128
+ parser = GridArgumentParser()
129
+ parser.add_argument("--hparam1", type=int, searchable=True)
130
+ parser.add_argument("--hparam2", type=int, searchable=True)
131
+ parser.add_argument("--hparam3", type=int, searchable=True, default=1000)
132
+ parser.add_argument("--hparam4", type=int, searchable=True, default=2000)
133
+ parser.add_argument("--normal", required=True, type=str)
134
+
135
+ args = parser.parse_args(
136
+ (
137
+ "--hparam1 1 2 "
138
+ "{--hparam2 1 2 3 {--normal normal --hparam4 100 101 102} {--normal maybe --hparam4 200 201 202 203}} "
139
+ "{--hparam2 4 5 6 --normal not-normal}"
140
+ ).split()
141
+ )
142
+ assert len(args) == 2 * ((3 * (3 + 4)) + 3)
143
+ ```
144
+
145
+ ## Additional capabilities
146
+
147
+ ### Configuration files
148
+
149
+ Using `omegaconf` (the only dependency), we allow users to specify (potentially multiple) configuration files that can be used to populate the resulting namespace(s). Access the through the `gridparse-config` argument: `--gridparse-config /this/config.json /that/config.yml`. Command-line arguments are given higher priority, and then the priority is in order of appearance in the command line for the configuration files.
150
+
151
+ ### Specify `None` in command-line
152
+
153
+ In case some parameter is searchable (and not a boolean), you might need one of the values to be the default value `None`. In that case, specifying any other value would rule the value `None` out from the grid search. To avoid this, `gridparse` allows you to specify the value `_None_` in the command line:
154
+
155
+ ```python
156
+ >>> parser = gridparse.GridArgumentParser()
157
+ >>> parser.add_argument('--text', type=str, searchable=True)
158
+ >>> parser.parse_args("--text a b _None_".split())
159
+ [Namespace(text='a'), Namespace(text='b'), Namespace(text=None)]
160
+ ```
161
+
162
+ ### Access values of other parameter
163
+
164
+ Moreover, you can use the value (not the default) of another argument as the default by setting the default to `args.<name-of-other-argument>`.
165
+
166
+ ```python
167
+ >>> parser = gridparse.GridArgumentParser()
168
+ >>> parser.add_argument('--num', type=int, searchable=True)
169
+ >>> parser.add_argument('--other-num', type=int, searchable=True, default="args.num")
170
+ >>> parser.parse_args("--num 1 2".split())
171
+ [Namespace(num=1, other_num=1), Namespace(num=2, other_num=2)]
172
+ ```
173
+
174
+ You can also specify so in the command line, i.e., `args.<name-of-other-argument>` does not have to appear in the default value of the argument.
175
+
176
+ This allows you the flexibility to have a parameter default to another parameter's values, and then specify different values when need arises (example use case: specify different CUDA device for a specific component only when OOM errors are encountered, and have it default to the "general" device otherwise).
177
+
178
+ ### Different value for each dataset split
179
+
180
+ You can specify the kw argument `splits` to create one argument per split:
181
+
182
+ ```python
183
+ >>> parser = gridparse.GridArgumentParser()
184
+ >>> parser.add_argument('--num', type=int, searchable=True)
185
+ >>> parser.add_argument('--other-num', type=int, splits=["train", "test"])
186
+ >>> parser.parse_args("--num 1 2 --train-other-num 3 --test-other-num 5".split())
187
+ [Namespace(num=1, test_other_num=5, train_other_num=3), Namespace(num=2, test_other_num=5, train_other_num=3)]
188
+ ```
189
+
190
+ Note that if an underscore (`_`) exists in the name of the argument, the new names will also join the splits with the original name with an underscore: `--other_num` to `--train_other_num`, etc. The new arguments are separate, i.e. if searchable, you do *not* have to specify the same number of values, etc. They each gain all the properties specified in the original argument.
@@ -0,0 +1,13 @@
1
+ LICENSE
2
+ MANIFEST.in
3
+ README.md
4
+ pyproject.toml
5
+ setup.py
6
+ gridparse/__init__.py
7
+ gridparse/grid_argument_parser.py
8
+ gridparse/utils.py
9
+ gridparse.egg-info/PKG-INFO
10
+ gridparse.egg-info/SOURCES.txt
11
+ gridparse.egg-info/dependency_links.txt
12
+ gridparse.egg-info/requires.txt
13
+ gridparse.egg-info/top_level.txt
@@ -0,0 +1,5 @@
1
+ omegaconf
2
+
3
+ [dev]
4
+ black
5
+ pytest
@@ -0,0 +1 @@
1
+ gridparse
@@ -0,0 +1,52 @@
1
+ [project]
2
+ name = "gridparse"
3
+ version = "1.5.0"
4
+ description = "Grid search directly from argparse"
5
+ readme = "README.md"
6
+ requires-python = ">=3.7"
7
+ license = {file = "LICENSE"}
8
+ keywords = ["machine learning", "grid search", "argparse"]
9
+ authors = [
10
+ {name = "Georgios (Yiorgos) Chochlakis", email = "georgioschochlakis@gmail.com" }
11
+ ]
12
+ maintainers = [
13
+ {name = "Georgios (Yiorgos) Chochlakis", email = "georgioschochlakis@gmail.com" }
14
+ ]
15
+ classifiers = [
16
+ # Common values are
17
+ # 3 - Alpha
18
+ # 4 - Beta
19
+ # 5 - Production/Stable
20
+ "Development Status :: 3 - Alpha",
21
+
22
+ # Pick your license as you wish
23
+ "License :: OSI Approved :: MIT License",
24
+
25
+ "Programming Language :: Python :: 3",
26
+ "Programming Language :: Python :: 3.7",
27
+ "Programming Language :: Python :: 3.8",
28
+ "Programming Language :: Python :: 3.9",
29
+ "Programming Language :: Python :: 3.10",
30
+ "Programming Language :: Python :: 3.11",
31
+ "Programming Language :: Python :: 3 :: Only",
32
+ ]
33
+
34
+
35
+ dependencies = [
36
+ "omegaconf",
37
+ ]
38
+
39
+ [project.optional-dependencies]
40
+ dev = ["black", "pytest"]
41
+
42
+ [project.urls]
43
+ "Homepage" = "https://github.com/gchochla/gridparse"
44
+ "Bug Reports" = "https://github.com/gchochla/gridparse/issues"
45
+ "Source" = "https://github.com/gchochla/gridparse"
46
+
47
+ [build-system]
48
+ requires = [
49
+ "setuptools>=61.0.0",
50
+ "wheel"
51
+ ]
52
+ build-backend = "setuptools.build_meta"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,18 @@
1
+ from setuptools import setup, find_packages
2
+
3
+ with open("README.md", "r", encoding="utf-8") as fh:
4
+ long_description = fh.read()
5
+
6
+ setup(
7
+ name="gridparse",
8
+ version="1.5.0",
9
+ description="Grid search directly from argparse",
10
+ author="Georgios Chochlakis",
11
+ author_email="georgioschochlakis@gmail.com",
12
+ long_description=long_description,
13
+ long_description_content_type="text/markdown",
14
+ url="https://github.com/gchochla/gridparse",
15
+ packages=find_packages(),
16
+ install_requires=["omegaconf"],
17
+ extras_require={"dev": ["black", "pytest"]},
18
+ )