gridparse 1.5.0__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.
- gridparse/__init__.py +4 -0
- gridparse/grid_argument_parser.py +539 -0
- gridparse/utils.py +52 -0
- gridparse-1.5.0.dist-info/LICENSE +21 -0
- gridparse-1.5.0.dist-info/METADATA +190 -0
- gridparse-1.5.0.dist-info/RECORD +8 -0
- gridparse-1.5.0.dist-info/WHEEL +5 -0
- gridparse-1.5.0.dist-info/top_level.txt +1 -0
gridparse/__init__.py
ADDED
@@ -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
|
gridparse/utils.py
ADDED
@@ -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,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,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` → `--arg 1 2 3`
|
62
|
+
|
63
|
+
`--arg 1 2 3` → `--arg 1~~2~~3 4~~5~~6`
|
64
|
+
|
65
|
+
`--arg 1-2-3 4-5-6` → `--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,8 @@
|
|
1
|
+
gridparse/__init__.py,sha256=Gxs7fWUO4SeeNeNiWMEOMOjq9i-VY1e-IMHo4dtuDYw,124
|
2
|
+
gridparse/grid_argument_parser.py,sha256=GZKVP6Mnwlc7oSTirDEnYoZRVy--0Oq98_cnP0Ase8Y,20316
|
3
|
+
gridparse/utils.py,sha256=kw0MuXUKwndecYEqAD3deDQvqJSepjE1kpZ-8ToP07A,1507
|
4
|
+
gridparse-1.5.0.dist-info/LICENSE,sha256=ZgtTDcMoDqp_dRh-dcUawX2b7ui7yJfQ2x2pvPHkFxQ,1075
|
5
|
+
gridparse-1.5.0.dist-info/METADATA,sha256=xNmFwtOVWdU8zj0ozWIZ5n4_VddgmXNIJyjG2m8osWo,9102
|
6
|
+
gridparse-1.5.0.dist-info/WHEEL,sha256=beeZ86-EfXScwlR_HKu4SllMC9wUEj_8Z_4FJ3egI2w,91
|
7
|
+
gridparse-1.5.0.dist-info/top_level.txt,sha256=c1TQokcB-by0gkB-VfQNnu7njcKO2giM7zyo4W-6a7k,10
|
8
|
+
gridparse-1.5.0.dist-info/RECORD,,
|
@@ -0,0 +1 @@
|
|
1
|
+
gridparse
|