nextmv 0.23.0__py3-none-any.whl → 0.24.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.
nextmv/options.py CHANGED
@@ -6,14 +6,19 @@ import copy
6
6
  import json
7
7
  import os
8
8
  from dataclasses import dataclass
9
- from typing import Any, Optional
9
+ from typing import Any, Optional, Union
10
10
 
11
11
  from nextmv.base_model import BaseModel
12
+ from nextmv.deprecated import deprecated
12
13
 
13
14
 
14
15
  @dataclass
15
16
  class Parameter:
16
17
  """
18
+ DEPRECATION WARNING
19
+ ----------
20
+ `Parameter` is deprecated, use `Option` instead.
21
+
17
22
  Parameter that is used in a `Configuration`. When a parameter is required,
18
23
  it is a good practice to provide a default value for it. This is because
19
24
  the configuration will raise an error if a required parameter is not
@@ -58,9 +63,20 @@ class Parameter:
58
63
  choices: list[Optional[Any]] = None
59
64
  """Limits values to a specific set of choices."""
60
65
 
66
+ def __post_init__(self):
67
+ deprecated(
68
+ name="Parameter",
69
+ reason="`Parameter` is deprecated, use `Option` instead.",
70
+ )
71
+
61
72
  @classmethod
62
73
  def from_dict(cls, data: dict[str, Any]) -> "Parameter":
63
74
  """
75
+ DEPRECATION WARNING
76
+ ----------
77
+ `Parameter` is deprecated, use `Option` instead. Parameter.from_dict ->
78
+ Option.from_dict
79
+
64
80
  Creates an instance of `Parameter` from a dictionary.
65
81
 
66
82
  Parameters
@@ -74,6 +90,11 @@ class Parameter:
74
90
  An instance of `Parameter`.
75
91
  """
76
92
 
93
+ deprecated(
94
+ name="Parameter.from_dict",
95
+ reason="`Parameter` is deprecated, use `Option` instead. Parameter.from_dict -> Option.from_dict",
96
+ )
97
+
77
98
  param_type_string = data["param_type"]
78
99
  param_type = getattr(builtins, param_type_string.split("'")[1])
79
100
 
@@ -88,6 +109,11 @@ class Parameter:
88
109
 
89
110
  def to_dict(self) -> dict[str, Any]:
90
111
  """
112
+ DEPRECATION WARNING
113
+ ----------
114
+ `Parameter` is deprecated, use `Option` instead. Parameter.to_dict ->
115
+ Option.to_dict
116
+
91
117
  Converts the parameter to a dict.
92
118
 
93
119
  Returns
@@ -96,6 +122,11 @@ class Parameter:
96
122
  The parameter as a dict.
97
123
  """
98
124
 
125
+ deprecated(
126
+ name="Parameter.to_dict",
127
+ reason="`Parameter` is deprecated, use `Option` instead. Parameter.to_dict -> Option.to_dict",
128
+ )
129
+
99
130
  return {
100
131
  "name": self.name,
101
132
  "param_type": str(self.param_type),
@@ -106,19 +137,120 @@ class Parameter:
106
137
  }
107
138
 
108
139
 
140
+ @dataclass
141
+ class Option:
142
+ """
143
+ `Option` that is used in `Options`. When an `Option` is required,
144
+ it is a good practice to provide a default value for it. This is because
145
+ the `Options` will raise an error if a required `Option` is not
146
+ provided through a command-line argument, an environment variable or a
147
+ default value.
148
+
149
+ Attributes
150
+ ----------
151
+ name : str
152
+ The name of the option.
153
+ option_type : type
154
+ The type of the option.
155
+ default : Any, optional
156
+ The default value of the option. Even though this is optional, it is
157
+ recommended to provide a default value for all options.
158
+ description : str, optional
159
+ An optional description of the option. This is useful for generating
160
+ help messages for the `Options`.
161
+ required : bool, optional
162
+ Whether the option is required. If an option is required, it will
163
+ be an error to not provide a value for it, either trough a command-line
164
+ argument, an environment variable or a default value.
165
+ choices : list[Optional[Any]], optional
166
+ Limits values to a specific set of choices.
167
+ """
168
+
169
+ name: str
170
+ """The name of the option."""
171
+ option_type: type
172
+ """The type of the option."""
173
+
174
+ default: Optional[Any] = None
175
+ """
176
+ The default value of the option. Even though this is optional, it is
177
+ recommended to provide a default value for all options.
178
+ """
179
+ description: Optional[str] = None
180
+ """
181
+ An optional description of the option. This is useful for generating help
182
+ messages for the `Options`.
183
+ """
184
+ required: bool = False
185
+ """
186
+ Whether the option is required. If a option is required, it will be an
187
+ error to not provide a value for it, either trough a command-line argument,
188
+ an environment variable or a default value.
189
+ """
190
+ choices: Optional[list[Any]] = None
191
+ """Limits values to a specific set of choices."""
192
+
193
+ @classmethod
194
+ def from_dict(cls, data: dict[str, Any]) -> "Option":
195
+ """
196
+ Creates an instance of `Option` from a dictionary.
197
+
198
+ Parameters
199
+ ----------
200
+ data : dict[str, Any]
201
+ The dictionary representation of an option.
202
+
203
+ Returns
204
+ -------
205
+ Option
206
+ An instance of `Option`.
207
+ """
208
+
209
+ option_type_string = data["option_type"]
210
+ option_type = getattr(builtins, option_type_string.split("'")[1])
211
+
212
+ return Option(
213
+ name=data["name"],
214
+ option_type=option_type,
215
+ default=data.get("default"),
216
+ description=data.get("description"),
217
+ required=data.get("required", False),
218
+ choices=data.get("choices"),
219
+ )
220
+
221
+ def to_dict(self) -> dict[str, Any]:
222
+ """
223
+ Converts the option to a dict.
224
+
225
+ Returns
226
+ -------
227
+ dict[str, Any]
228
+ The option as a dict.
229
+ """
230
+
231
+ return {
232
+ "name": self.name,
233
+ "option_type": str(self.option_type),
234
+ "default": self.default,
235
+ "description": self.description,
236
+ "required": self.required,
237
+ "choices": self.choices,
238
+ }
239
+
240
+
109
241
  class Options:
110
242
  """
111
- Options for a run. To initialize options, pass in one or more `Parameter`
243
+ Options for a run. To initialize options, pass in one or more `Option`
112
244
  objects. The options will look for the values of the given parameters in
113
245
  the following order: command-line arguments, environment variables, default
114
246
  values.
115
247
 
116
- Once the options are initialized, you can access the parameters as
117
- attributes of the `Options` object. For example, if you have a
118
- `Parameter` object with the name "duration", you can access it as
248
+ Once the `Options` are initialized, you can access the underlying options as
249
+ attributes of the `Options` object. For example, if you have an
250
+ `Option` object with the name "duration", you can access it as
119
251
  `options.duration`.
120
252
 
121
- If a parameter is required and not provided through a command-line
253
+ If an option is required and not provided through a command-line
122
254
  argument, an environment variable or a default value, an error will be
123
255
  raised.
124
256
 
@@ -132,19 +264,19 @@ class Options:
132
264
  be merged with other options. After options are parsed, you may get the
133
265
  help message by running the script with the `-h/--help` flag.
134
266
 
135
- Parameters
267
+ Attributes
136
268
  ----------
137
- *parameters : Parameter
138
- The parameters that are used in the options. At least one
139
- parameter is required.
269
+ *options : Option
270
+ The list of `Option` objects that are used in the options. At least one
271
+ option is required.
140
272
 
141
273
  Examples
142
274
  --------
143
275
  >>> import nextmv
144
276
  >>>
145
277
  >>> options = nextmv.Options(
146
- ... nextmv.Parameter("duration", str, "30s", description="solver duration", required=False),
147
- ... nextmv.Parameter("threads", int, 4, description="computer threads", required=False),
278
+ ... nextmv.Option("duration", str, "30s", description="solver duration", required=False),
279
+ ... nextmv.Option("threads", int, 4, description="computer threads", required=False),
148
280
  ... )
149
281
  >>>
150
282
  >>> print(options.duration, options.threads, options.to_dict())
@@ -154,10 +286,11 @@ class Options:
154
286
  Raises
155
287
  ------
156
288
  ValueError
157
- If a required parameter is not provided through a command-line
289
+ If a required option is not provided through a command-line
158
290
  argument, an environment variable or a default value.
159
291
  TypeError
160
- If a parameter is not a `Parameter` object.
292
+ If an option is not either an `Option` or `Parameter` (deprecated)
293
+ object.
161
294
  ValueError
162
295
  If an environment variable is not of the type of the corresponding
163
296
  parameter.
@@ -165,10 +298,10 @@ class Options:
165
298
 
166
299
  PARSED = False
167
300
 
168
- def __init__(self, *parameters: Parameter):
301
+ def __init__(self, *options: Option):
169
302
  """Initializes the options."""
170
303
 
171
- self.parameters = copy.deepcopy(parameters)
304
+ self.options = copy.deepcopy(options)
172
305
 
173
306
  def to_dict(self) -> dict[str, Any]:
174
307
  """
@@ -190,7 +323,7 @@ class Options:
190
323
 
191
324
  self_dict = copy.deepcopy(self.__dict__)
192
325
 
193
- rm_keys = ["parameters", "PARSED"]
326
+ rm_keys = ["PARSED", "options"]
194
327
  for key in rm_keys:
195
328
  if key in self_dict:
196
329
  self_dict.pop(key)
@@ -199,7 +332,7 @@ class Options:
199
332
 
200
333
  return m.to_dict()["config"]
201
334
 
202
- def to_cloud_dict(self) -> dict[str, str]:
335
+ def to_dict_cloud(self) -> dict[str, str]:
203
336
  """
204
337
  Converts the options to a dict that can be used in the Nextmv Cloud.
205
338
  Cloud has a hard requirement that options are passed as strings. This
@@ -227,6 +360,11 @@ class Options:
227
360
 
228
361
  def parameters_dict(self) -> list[dict[str, Any]]:
229
362
  """
363
+ DEPRECATION WARNING
364
+ ----------
365
+ `Parameter` is deprecated, use `Option` instead. Options.parameters_dict
366
+ -> Options.options_dict
367
+
230
368
  Converts the options to a list of dicts. Each dict is the dict
231
369
  representation of a `Parameter`.
232
370
 
@@ -236,7 +374,25 @@ class Options:
236
374
  The list of dictionaries (parameter entries).
237
375
  """
238
376
 
239
- return [param.to_dict() for param in self.parameters]
377
+ deprecated(
378
+ name="Options.parameters_dict",
379
+ reason="`Parameter` is deprecated, use `Option` instead. Options.parameters_dict -> Options.options_dict",
380
+ )
381
+
382
+ return [param.to_dict() for param in self.options]
383
+
384
+ def options_dict(self) -> list[dict[str, Any]]:
385
+ """
386
+ Converts the `Options` to a list of dicts. Each dict is the dict
387
+ representation of an `Option`.
388
+
389
+ Returns
390
+ -------
391
+ list[dict[str, Any]]
392
+ The list of dictionaries (`Option` entries).
393
+ """
394
+
395
+ return [opt.to_dict() for opt in self.options]
240
396
 
241
397
  def parse(self):
242
398
  """
@@ -257,8 +413,8 @@ class Options:
257
413
  >>> import nextmv
258
414
  >>>
259
415
  >>> options = nextmv.Options(
260
- ... nextmv.Parameter("duration", str, "30s", description="solver duration", required=False),
261
- ... nextmv.Parameter("threads", int, 4, description="computer threads", required=False),
416
+ ... nextmv.Option("duration", str, "30s", description="solver duration", required=False),
417
+ ... nextmv.Option("threads", int, 4, description="computer threads", required=False),
262
418
  ... )
263
419
  >>> options.parse() # Does not raise an exception.
264
420
 
@@ -267,8 +423,8 @@ class Options:
267
423
  >>> import nextmv
268
424
  >>>
269
425
  >>> options = nextmv.Options(
270
- ... nextmv.Parameter("duration", str, "30s", description="solver duration", required=False),
271
- ... nextmv.Parameter("threads", int, 4, description="computer threads", required=False),
426
+ ... nextmv.Option("duration", str, "30s", description="solver duration", required=False),
427
+ ... nextmv.Option("threads", int, 4, description="computer threads", required=False),
272
428
  ... )
273
429
  >>> print(options.duration) # Parses the options.
274
430
  >>> options.parse() # Raises an exception because the options have already been parsed.
@@ -278,10 +434,10 @@ class Options:
278
434
  RuntimeError
279
435
  If the options have already been parsed.
280
436
  ValueError
281
- If a required parameter is not provided through a command-line
437
+ If a required option is not provided through a command-line
282
438
  argument, an environment variable or a default value.
283
439
  TypeError
284
- If a parameter is not a `Parameter` object.
440
+ If an option is not an `Option` or `Parameter` (deprecated) object.
285
441
  ValueError
286
442
  If an environment variable is not of the type of the corresponding
287
443
  parameter.
@@ -328,7 +484,7 @@ class Options:
328
484
  "new options have already been parsed, cannot merge. See `Options.parse()` for more information."
329
485
  )
330
486
 
331
- self.parameters += new.parameters
487
+ self.options += new.options
332
488
 
333
489
  self._parse()
334
490
 
@@ -356,16 +512,21 @@ class Options:
356
512
  An instance of `Options`.
357
513
  """
358
514
 
359
- parameters = []
515
+ options = []
360
516
  for key, value in data.items():
361
- parameter = Parameter(name=key, param_type=type(value), default=value)
362
- parameters.append(parameter)
517
+ opt = Option(name=key, option_type=type(value), default=value)
518
+ options.append(opt)
363
519
 
364
- return cls(*parameters)
520
+ return cls(*options)
365
521
 
366
522
  @classmethod
367
523
  def from_parameters_dict(cls, parameters_dict: list[dict[str, Any]]) -> "Options":
368
524
  """
525
+ DEPRECATION WARNING
526
+ ----------
527
+ `Parameter` is deprecated, use `Option` instead. Options.from_parameters_dict
528
+ -> Options.from_options_dict
529
+
369
530
  Creates an instance of `Options` from parameters in dict form. Each
370
531
  entry is the dict representation of a `Parameter`.
371
532
 
@@ -380,6 +541,12 @@ class Options:
380
541
  An instance of `Options`.
381
542
  """
382
543
 
544
+ deprecated(
545
+ name="Options.from_parameters_dict",
546
+ reason="`Parameter` is deprecated, use `Option` instead. "
547
+ "Options.from_parameters_dict -> Options.from_options_dict",
548
+ )
549
+
383
550
  parameters = []
384
551
  for parameter_dict in parameters_dict:
385
552
  parameter = Parameter.from_dict(parameter_dict)
@@ -387,6 +554,30 @@ class Options:
387
554
 
388
555
  return cls(*parameters)
389
556
 
557
+ @classmethod
558
+ def from_options_dict(cls, options_dict: list[dict[str, Any]]) -> "Options":
559
+ """
560
+ Creates an instance of `Options` from a list of `Option` objects in
561
+ dict form. Each entry is the dict representation of an `Option`.
562
+
563
+ Parameters
564
+ ----------
565
+ data : list[dict[str, Any]]
566
+ The list of dictionaries (`Option` entries).
567
+
568
+ Returns
569
+ -------
570
+ Options
571
+ An instance of `Options`.
572
+ """
573
+
574
+ options = []
575
+ for opt_dict in options_dict:
576
+ opt = Option.from_dict(opt_dict)
577
+ options.append(opt)
578
+
579
+ return cls(*options)
580
+
390
581
  def __getattr__(self, name: str) -> Any:
391
582
  """
392
583
  Gets an attribute of the options. This is called when an attribute
@@ -406,10 +597,10 @@ class Options:
406
597
  Raises
407
598
  ------
408
599
  ValueError
409
- If a required parameter is not provided through a command-line
600
+ If a required option is not provided through a command-line
410
601
  argument, an environment variable or a default value.
411
602
  TypeError
412
- If a parameter is not a `Parameter` object.
603
+ If an option is not an `Option` or `Parameter` (deprecated) object.
413
604
  ValueError
414
605
  If an environment variable is not of the type of the corresponding
415
606
  parameter.
@@ -417,7 +608,7 @@ class Options:
417
608
 
418
609
  self.PARSED = True
419
610
 
420
- if not self.parameters:
611
+ if not self.options:
421
612
  return
422
613
 
423
614
  parser = argparse.ArgumentParser(
@@ -427,41 +618,43 @@ class Options:
427
618
  + "or environment variables.",
428
619
  allow_abbrev=False,
429
620
  )
430
- params_by_field_name: dict[str, Parameter] = {}
621
+ options_by_field_name: dict[str, Option] = {}
431
622
 
432
- for p, param in enumerate(self.parameters):
433
- if not isinstance(param, Parameter):
434
- raise TypeError(f"expected a <Parameter> object, but got {type(param)} in index {p}")
623
+ for ix, option in enumerate(self.options):
624
+ if not isinstance(option, Option) and not isinstance(option, Parameter):
625
+ raise TypeError(
626
+ f"expected an <Option> (or deprecated <Parameter>) object, but got {type(option)} in index {ix}"
627
+ )
435
628
 
436
629
  # See comment below about ipykernel adding a `-f` argument. We
437
- # restrict parameters from having the name 'f' or 'fff' for that
630
+ # restrict options from having the name 'f' or 'fff' for that
438
631
  # reason.
439
- if param.name == "f" or param.name == "fff":
440
- raise ValueError("parameter names 'f', 'fff' are reserved for internal use")
632
+ if option.name == "f" or option.name == "fff":
633
+ raise ValueError("option names 'f', 'fff' are reserved for internal use")
441
634
 
442
- if param.name == "PARSED":
443
- raise ValueError("parameter name 'PARSED' is reserved for internal use")
635
+ if option.name == "PARSED":
636
+ raise ValueError("option name 'PARSED' is reserved for internal use")
444
637
 
445
638
  # Remove any leading '-'. This is in line with argparse's behavior.
446
- param.name = param.name.lstrip("-")
639
+ option.name = option.name.lstrip("-")
447
640
 
448
641
  kwargs = {
449
- "type": param.param_type if param.param_type is not bool else str,
450
- "help": self._description(param),
642
+ "type": self._option_type(option) if self._option_type(option) is not bool else str,
643
+ "help": self._description(option),
451
644
  }
452
645
 
453
- if param.choices is not None:
454
- kwargs["choices"] = param.choices
646
+ if option.choices is not None:
647
+ kwargs["choices"] = option.choices
455
648
 
456
649
  parser.add_argument(
457
- f"-{param.name}",
458
- f"--{param.name}",
650
+ f"-{option.name}",
651
+ f"--{option.name}",
459
652
  **kwargs,
460
653
  )
461
654
 
462
- # Store the parameter by its field name for easy access later. argparse
655
+ # Store the option by its field name for easy access later. argparse
463
656
  # replaces '-' with '_', so we do the same here.
464
- params_by_field_name[param.name.replace("-", "_")] = param
657
+ options_by_field_name[option.name.replace("-", "_")] = option
465
658
 
466
659
  # The ipkyernel uses a `-f` argument by default that it passes to the
467
660
  # execution. We don’t want to ignore this argument because we get an
@@ -479,67 +672,73 @@ class Options:
479
672
  if arg == "fff" or arg == "f":
480
673
  continue
481
674
 
482
- param = params_by_field_name[arg]
675
+ option = options_by_field_name[arg]
483
676
 
484
- # First, attempt to set the value of a parameter from the
677
+ # First, attempt to set the value of an option from the
485
678
  # command-line args.
486
679
  arg_value = getattr(args, arg)
487
680
  if arg_value is not None:
488
- value = self._parameter_value(param, arg_value)
681
+ value = self._option_value(option, arg_value)
489
682
  setattr(self, arg, value)
490
683
  continue
491
684
 
492
- # Second, attempt to set the value of a parameter from the
685
+ # Second, attempt to set the value of am option from the
493
686
  # environment variables.
494
687
  upper_name = arg.upper()
495
688
  env_value = os.getenv(upper_name)
496
689
  if env_value is not None:
497
690
  try:
498
- typed_env_value = param.param_type(env_value) if param.param_type is not bool else env_value
691
+ typed_env_value = (
692
+ self._option_type(option)(env_value) if self._option_type(option) is not bool else env_value
693
+ )
499
694
  except ValueError:
500
- raise ValueError(f'environment variable "{upper_name}" is not of type {param.param_type}') from None
695
+ raise ValueError(
696
+ f'environment variable "{upper_name}" is not of type {self._option_type(option)}'
697
+ ) from None
501
698
 
502
- value = self._parameter_value(param, typed_env_value)
699
+ value = self._option_value(option, typed_env_value)
503
700
  setattr(self, arg, value)
504
701
  continue
505
702
 
506
703
  # Finally, attempt to set a default value. This is only allowed
507
- # for non-required parameters.
508
- if not param.required:
509
- setattr(self, arg, param.default)
704
+ # for non-required options.
705
+ if not option.required:
706
+ setattr(self, arg, option.default)
510
707
  continue
511
708
 
512
- # At this point, the parameter is required and no value was
709
+ # At this point, the option is required and no value was
513
710
  # provided
514
711
  raise ValueError(
515
- f'parameter "{arg}" is required but not provided through: command-line args, env vars, or default value'
712
+ f'option "{arg}" is required but not provided through: command-line args, env vars, or default value'
516
713
  )
517
714
 
518
- @staticmethod
519
- def _description(param: Parameter) -> str:
520
- """Returns a description for a parameter."""
715
+ def _description(self, option: Option) -> str:
716
+ """Returns a description for an option."""
717
+
718
+ description = ""
719
+ if isinstance(option, Parameter):
720
+ description = "DEPRECATED (initialized with <Parameter>, use <Option> instead) "
521
721
 
522
- description = f"[env var: {param.name.upper()}]"
722
+ description += f"[env var: {option.name.upper()}]"
523
723
 
524
- if param.required:
724
+ if option.required:
525
725
  description += " (required)"
526
726
 
527
- if param.default is not None:
528
- description += f" (default: {param.default})"
727
+ if option.default is not None:
728
+ description += f" (default: {option.default})"
529
729
 
530
- description += f" (type: {param.param_type.__name__})"
730
+ description += f" (type: {self._option_type(option).__name__})"
531
731
 
532
- if param.description is not None:
533
- description += f": {param.description}"
732
+ if option.description is not None and option.description != "":
733
+ description += f": {option.description}"
534
734
 
535
735
  return description
536
736
 
537
- @staticmethod
538
- def _parameter_value(parameter: Parameter, value: Any) -> Any:
539
- """Handles how the value of a parameter is extracted."""
737
+ def _option_value(self, option: Option, value: Any) -> Any:
738
+ """Handles how the value of an option is extracted."""
540
739
 
541
- param_type = parameter.param_type
542
- if param_type is not bool:
740
+ opt_type = self._option_type(option)
741
+ if opt_type is not bool:
543
742
  return value
544
743
 
545
744
  value = str(value).lower()
@@ -548,3 +747,19 @@ class Options:
548
747
  return True
549
748
 
550
749
  return False
750
+
751
+ @staticmethod
752
+ def _option_type(option: Union[Option, Parameter]) -> type:
753
+ """Auxiliary function for handling the type of an option. This function
754
+ was introduced for backwards compatibility with the deprecated
755
+ `Parameter` class. Once `Parameter` is removed, this function can be removed
756
+ as well. When the function is removed, use the `option.option_type`
757
+ attribute directly, instead of calling this function.
758
+ """
759
+
760
+ if isinstance(option, Option):
761
+ return option.option_type
762
+ elif isinstance(option, Parameter):
763
+ return option.param_type
764
+ else:
765
+ raise TypeError(f"expected an <Option> (or deprecated <Parameter>) object, but got {type(option)}")