metaflow 2.12.39__py2.py3-none-any.whl → 2.13__py2.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.
Files changed (34) hide show
  1. metaflow/__init__.py +1 -1
  2. metaflow/cli.py +111 -36
  3. metaflow/cli_args.py +2 -2
  4. metaflow/cli_components/run_cmds.py +3 -1
  5. metaflow/datastore/flow_datastore.py +2 -2
  6. metaflow/exception.py +8 -2
  7. metaflow/flowspec.py +48 -36
  8. metaflow/graph.py +28 -27
  9. metaflow/includefile.py +2 -2
  10. metaflow/lint.py +35 -20
  11. metaflow/metaflow_config.py +5 -0
  12. metaflow/parameters.py +11 -4
  13. metaflow/plugins/argo/argo_workflows_deployer_objects.py +42 -0
  14. metaflow/plugins/aws/secrets_manager/aws_secrets_manager_secrets_provider.py +13 -10
  15. metaflow/plugins/cards/card_creator.py +1 -0
  16. metaflow/plugins/cards/card_decorator.py +46 -8
  17. metaflow/plugins/pypi/bootstrap.py +196 -61
  18. metaflow/plugins/pypi/conda_environment.py +76 -21
  19. metaflow/plugins/pypi/micromamba.py +42 -15
  20. metaflow/plugins/pypi/pip.py +8 -3
  21. metaflow/runner/click_api.py +175 -39
  22. metaflow/runner/deployer_impl.py +6 -1
  23. metaflow/runner/metaflow_runner.py +6 -1
  24. metaflow/runner/utils.py +5 -0
  25. metaflow/user_configs/config_options.py +87 -34
  26. metaflow/user_configs/config_parameters.py +44 -25
  27. metaflow/util.py +2 -2
  28. metaflow/version.py +1 -1
  29. {metaflow-2.12.39.dist-info → metaflow-2.13.dist-info}/METADATA +2 -2
  30. {metaflow-2.12.39.dist-info → metaflow-2.13.dist-info}/RECORD +34 -34
  31. {metaflow-2.12.39.dist-info → metaflow-2.13.dist-info}/LICENSE +0 -0
  32. {metaflow-2.12.39.dist-info → metaflow-2.13.dist-info}/WHEEL +0 -0
  33. {metaflow-2.12.39.dist-info → metaflow-2.13.dist-info}/entry_points.txt +0 -0
  34. {metaflow-2.12.39.dist-info → metaflow-2.13.dist-info}/top_level.txt +0 -0
@@ -18,6 +18,7 @@ import json
18
18
  from collections import OrderedDict
19
19
  from typing import Any, Callable, Dict, List, Optional
20
20
  from typing import OrderedDict as TOrderedDict
21
+ from typing import Tuple as TTuple
21
22
  from typing import Union
22
23
 
23
24
  from metaflow import FlowSpec, Parameter
@@ -38,8 +39,16 @@ from metaflow._vendor.typeguard import TypeCheckError, check_type
38
39
  from metaflow.decorators import add_decorator_options
39
40
  from metaflow.exception import MetaflowException
40
41
  from metaflow.includefile import FilePathClass
42
+ from metaflow.metaflow_config import CLICK_API_PROCESS_CONFIG
41
43
  from metaflow.parameters import JSONTypeClass, flow_context
42
- from metaflow.user_configs.config_options import LocalFileInput
44
+ from metaflow.user_configs.config_options import (
45
+ ConfigValue,
46
+ ConvertDictOrStr,
47
+ ConvertPath,
48
+ LocalFileInput,
49
+ MultipleTuple,
50
+ config_options_with_config_input,
51
+ )
43
52
 
44
53
  # Define a recursive type alias for JSON
45
54
  JSON = Union[Dict[str, "JSON"], List["JSON"], str, int, float, bool, None]
@@ -58,6 +67,7 @@ click_to_python_types = {
58
67
  JSONTypeClass: JSON,
59
68
  FilePathClass: str,
60
69
  LocalFileInput: str,
70
+ MultipleTuple: TTuple[str, Union[JSON, ConfigValue]],
61
71
  }
62
72
 
63
73
 
@@ -68,7 +78,7 @@ def _method_sanity_check(
68
78
  defaults: TOrderedDict[str, Any],
69
79
  **kwargs
70
80
  ) -> Dict[str, Any]:
71
- method_params = {"args": {}, "options": {}}
81
+ method_params = {"args": {}, "options": {}, "defaults": defaults}
72
82
 
73
83
  possible_params = OrderedDict()
74
84
  possible_params.update(possible_arg_params)
@@ -90,10 +100,26 @@ def _method_sanity_check(
90
100
  % (supplied_k, annotations[supplied_k], defaults[supplied_k])
91
101
  )
92
102
 
93
- # because Click expects stringified JSON..
94
- supplied_v = (
95
- json.dumps(supplied_v) if annotations[supplied_k] == JSON else supplied_v
96
- )
103
+ # Clean up values to make them into what click expects
104
+ if annotations[supplied_k] == JSON:
105
+ # JSON should be a string (json dumps)
106
+ supplied_v = json.dumps(supplied_v)
107
+ elif supplied_k == "config_value":
108
+ # Special handling of config value because we need to go look in the tuple
109
+ new_list = []
110
+ for cfg_name, cfg_value in supplied_v:
111
+ if isinstance(cfg_value, ConfigValue):
112
+ # ConfigValue should be JSONified and converted to a string
113
+ new_list.append((cfg_name, json.dumps(cfg_value.to_dict())))
114
+ elif isinstance(cfg_value, dict):
115
+ # ConfigValue passed as a dictionary
116
+ new_list.append((cfg_name, json.dumps(cfg_value)))
117
+ else:
118
+ raise TypeError(
119
+ "Invalid type for a config-value, expected a ConfigValue or "
120
+ "dict but got '%s'" % type(cfg_value)
121
+ )
122
+ supplied_v = new_list
97
123
 
98
124
  if supplied_k in possible_arg_params:
99
125
  cli_name = possible_arg_params[supplied_k].opts[0].strip("-")
@@ -188,35 +214,56 @@ loaded_modules = {}
188
214
  def extract_flow_class_from_file(flow_file: str) -> FlowSpec:
189
215
  if not os.path.exists(flow_file):
190
216
  raise FileNotFoundError("Flow file not present at '%s'" % flow_file)
191
- # Check if the module has already been loaded
192
- if flow_file in loaded_modules:
193
- module = loaded_modules[flow_file]
194
- else:
195
- # Load the module if it's not already loaded
196
- spec = importlib.util.spec_from_file_location("module", flow_file)
197
- module = importlib.util.module_from_spec(spec)
198
- spec.loader.exec_module(module)
199
- # Cache the loaded module
200
- loaded_modules[flow_file] = module
201
- classes = inspect.getmembers(module, inspect.isclass)
202
-
203
- flow_cls = None
204
- for _, kls in classes:
205
- if kls != FlowSpec and issubclass(kls, FlowSpec):
206
- if flow_cls is not None:
207
- raise MetaflowException(
208
- "Multiple FlowSpec classes found in %s" % flow_file
209
- )
210
- flow_cls = kls
211
-
212
- return flow_cls
217
+
218
+ flow_dir = os.path.dirname(os.path.abspath(flow_file))
219
+ path_was_added = False
220
+
221
+ # Only add to path if it's not already there
222
+ if flow_dir not in sys.path:
223
+ sys.path.insert(0, flow_dir)
224
+ path_was_added = True
225
+
226
+ try:
227
+ # Check if the module has already been loaded
228
+ if flow_file in loaded_modules:
229
+ module = loaded_modules[flow_file]
230
+ else:
231
+ # Load the module if it's not already loaded
232
+ spec = importlib.util.spec_from_file_location("module", flow_file)
233
+ module = importlib.util.module_from_spec(spec)
234
+ spec.loader.exec_module(module)
235
+ # Cache the loaded module
236
+ loaded_modules[flow_file] = module
237
+ classes = inspect.getmembers(module, inspect.isclass)
238
+
239
+ flow_cls = None
240
+ for _, kls in classes:
241
+ if kls != FlowSpec and issubclass(kls, FlowSpec):
242
+ if flow_cls is not None:
243
+ raise MetaflowException(
244
+ "Multiple FlowSpec classes found in %s" % flow_file
245
+ )
246
+ flow_cls = kls
247
+
248
+ if flow_cls is None:
249
+ raise MetaflowException("No FlowSpec class found in %s" % flow_file)
250
+ return flow_cls
251
+ finally:
252
+ # Only remove from path if we added it
253
+ if path_was_added:
254
+ try:
255
+ sys.path.remove(flow_dir)
256
+ except ValueError:
257
+ # User's code might have removed it already
258
+ pass
213
259
 
214
260
 
215
261
  class MetaflowAPI(object):
216
- def __init__(self, parent=None, flow_cls=None, **kwargs):
262
+ def __init__(self, parent=None, flow_cls=None, config_input=None, **kwargs):
217
263
  self._parent = parent
218
264
  self._chain = [{self._API_NAME: kwargs}]
219
265
  self._flow_cls = flow_cls
266
+ self._config_input = config_input
220
267
  self._cached_computed_parameters = None
221
268
 
222
269
  @property
@@ -238,11 +285,19 @@ class MetaflowAPI(object):
238
285
  flow_cls = extract_flow_class_from_file(flow_file)
239
286
 
240
287
  with flow_context(flow_cls) as _:
241
- add_decorator_options(cli_collection)
288
+ cli_collection, config_input = config_options_with_config_input(
289
+ cli_collection
290
+ )
291
+ cli_collection = add_decorator_options(cli_collection)
242
292
 
243
293
  def getattr_wrapper(_self, name):
244
294
  # Functools.partial do not automatically bind self (no __get__)
245
- return _self._internal_getattr(_self, name)
295
+ with flow_context(flow_cls) as _:
296
+ # We also wrap this in the proper flow context because since commands
297
+ # are loaded lazily, we need the proper flow context to compute things
298
+ # like parameters. If we do not do this, the outer flow's context will
299
+ # be used.
300
+ return _self._internal_getattr(_self, name)
246
301
 
247
302
  class_dict = {
248
303
  "__module__": "metaflow",
@@ -272,7 +327,12 @@ class MetaflowAPI(object):
272
327
  defaults,
273
328
  **kwargs,
274
329
  )
275
- return to_return(parent=None, flow_cls=flow_cls, **method_params)
330
+ return to_return(
331
+ parent=None,
332
+ flow_cls=flow_cls,
333
+ config_input=config_input,
334
+ **method_params,
335
+ )
276
336
 
277
337
  m = _method
278
338
  m.__name__ = cli_collection.name
@@ -313,8 +373,12 @@ class MetaflowAPI(object):
313
373
  for k, v in options.items():
314
374
  if isinstance(v, list):
315
375
  for i in v:
316
- components.append("--%s" % k)
317
- components.append(str(i))
376
+ if isinstance(i, tuple):
377
+ components.append("--%s" % k)
378
+ components.extend(map(str, i))
379
+ else:
380
+ components.append("--%s" % k)
381
+ components.append(str(i))
318
382
  else:
319
383
  components.append("--%s" % k)
320
384
  if v != "flag":
@@ -323,21 +387,93 @@ class MetaflowAPI(object):
323
387
  return components
324
388
 
325
389
  def _compute_flow_parameters(self):
326
- if self._flow_cls is None or self._parent is not None:
390
+ if (
391
+ self._flow_cls is None
392
+ or self._config_input is None
393
+ or self._parent is not None
394
+ ):
327
395
  raise RuntimeError(
328
396
  "Computing flow-level parameters for a non start API. "
329
397
  "Please report to the Metaflow team."
330
398
  )
331
- # TODO: We need to actually compute the new parameters (based on configs) which
332
- # would involve processing the options at least partially. We will do this
333
- # before GA but for now making it work for regular parameters
399
+
334
400
  if self._cached_computed_parameters is not None:
335
401
  return self._cached_computed_parameters
336
402
  self._cached_computed_parameters = []
403
+
404
+ config_options = None
405
+ if CLICK_API_PROCESS_CONFIG:
406
+ with flow_context(self._flow_cls) as _:
407
+ # We are going to resolve the configs first and then get the parameters.
408
+ # Note that configs may update/add parameters so the order is important
409
+ # Since part of the processing of configs happens by click, we need to
410
+ # "fake" it.
411
+
412
+ # Extract any config options as well as datastore and quiet options
413
+ method_params = self._chain[0][self._API_NAME]
414
+ opts = method_params["options"]
415
+ defaults = method_params["defaults"]
416
+
417
+ ds = opts.get("datastore", defaults["datastore"])
418
+ quiet = opts.get("quiet", defaults["quiet"])
419
+ is_default = False
420
+ config_file = opts.get("config-file")
421
+ if config_file is None:
422
+ is_default = True
423
+ config_file = defaults.get("config_file")
424
+
425
+ if config_file:
426
+ config_file = map(
427
+ lambda x: (x[0], ConvertPath.convert_value(x[1], is_default)),
428
+ config_file,
429
+ )
430
+
431
+ is_default = False
432
+ config_value = opts.get("config-value")
433
+ if config_value is None:
434
+ is_default = True
435
+ config_value = defaults.get("config_value")
436
+
437
+ if config_value:
438
+ config_value = map(
439
+ lambda x: (
440
+ x[0],
441
+ ConvertDictOrStr.convert_value(x[1], is_default),
442
+ ),
443
+ config_value,
444
+ )
445
+
446
+ if (config_file is None) ^ (config_value is None):
447
+ # If we have one, we should have the other
448
+ raise MetaflowException(
449
+ "Options were not properly set -- this is an internal error."
450
+ )
451
+
452
+ if config_file:
453
+ # Process both configurations; the second one will return all the merged
454
+ # configuration options properly processed.
455
+ self._config_input.process_configs(
456
+ self._flow_cls.__name__, "config_file", config_file, quiet, ds
457
+ )
458
+ config_options = self._config_input.process_configs(
459
+ self._flow_cls.__name__, "config_value", config_value, quiet, ds
460
+ )
461
+
462
+ # At this point, we are like in start() in cli.py -- we obtained the
463
+ # properly processed config_options which we can now use to process
464
+ # the config decorators (including CustomStep/FlowDecorators)
465
+ # Note that if CLICK_API_PROCESS_CONFIG is False, we still do this because
466
+ # it will init all parameters (config_options will be None)
467
+ # We ignore any errors if we don't check the configs in the click API.
468
+ new_cls = self._flow_cls._process_config_decorators(
469
+ config_options, ignore_errors=not CLICK_API_PROCESS_CONFIG
470
+ )
471
+ if new_cls:
472
+ self._flow_cls = new_cls
473
+
337
474
  for _, param in self._flow_cls._get_parameters():
338
475
  if param.IS_CONFIG_PARAMETER:
339
476
  continue
340
- param.init()
341
477
  self._cached_computed_parameters.append(param)
342
478
  return self._cached_computed_parameters
343
479
 
@@ -61,8 +61,13 @@ class DeployerImpl(object):
61
61
  "of DeployerImpl."
62
62
  )
63
63
 
64
+ from metaflow.parameters import flow_context
65
+
64
66
  if "metaflow.cli" in sys.modules:
65
- importlib.reload(sys.modules["metaflow.cli"])
67
+ # Reload the CLI with an "empty" flow -- this will remove any configuration
68
+ # options. They are re-added in from_cli (called below).
69
+ with flow_context(None) as _:
70
+ importlib.reload(sys.modules["metaflow.cli"])
66
71
  from metaflow.cli import start
67
72
  from metaflow.runner.click_api import MetaflowAPI
68
73
 
@@ -245,8 +245,13 @@ class Runner(object):
245
245
  # This ability is made possible by the statement:
246
246
  # 'from .metaflow_runner import Runner' in '__init__.py'
247
247
 
248
+ from metaflow.parameters import flow_context
249
+
248
250
  if "metaflow.cli" in sys.modules:
249
- importlib.reload(sys.modules["metaflow.cli"])
251
+ # Reload the CLI with an "empty" flow -- this will remove any configuration
252
+ # options. They are re-added in from_cli (called below).
253
+ with flow_context(None) as _:
254
+ importlib.reload(sys.modules["metaflow.cli"])
250
255
  from metaflow.cli import start
251
256
  from metaflow.runner.click_api import MetaflowAPI
252
257
 
metaflow/runner/utils.py CHANGED
@@ -111,6 +111,11 @@ def read_from_fifo_when_ready(
111
111
  poll.register(fifo_fd, select.POLLIN)
112
112
  max_timeout = 3 # Wait for 10 * 3 = 30 ms after last write
113
113
  while True:
114
+ if check_process_exited(command_obj) and command_obj.process.returncode != 0:
115
+ raise CalledProcessError(
116
+ command_obj.process.returncode, command_obj.command
117
+ )
118
+
114
119
  if timeout < 0:
115
120
  raise TimeoutError("Timeout while waiting for the file content")
116
121
 
@@ -15,7 +15,7 @@ from ..util import get_username
15
15
 
16
16
  _CONVERT_PREFIX = "@!c!@:"
17
17
  _DEFAULT_PREFIX = "@!d!@:"
18
- _NO_FILE = "@!n!@"
18
+ _NO_FILE = "@!n!@:"
19
19
 
20
20
  _CONVERTED_DEFAULT = _CONVERT_PREFIX + _DEFAULT_PREFIX
21
21
  _CONVERTED_NO_FILE = _CONVERT_PREFIX + _NO_FILE
@@ -41,7 +41,8 @@ class ConvertPath(click.Path):
41
41
  is_default = False
42
42
  if value and value.startswith(_DEFAULT_PREFIX):
43
43
  is_default = True
44
- value = super().convert(value[len(_DEFAULT_PREFIX) :], param, ctx)
44
+ value = value[len(_DEFAULT_PREFIX) :]
45
+ value = super().convert(value, param, ctx)
45
46
  return self.convert_value(value, is_default)
46
47
 
47
48
  @staticmethod
@@ -170,7 +171,15 @@ class ConfigInput:
170
171
  cls.loaded_configs = all_configs
171
172
  return cls.loaded_configs.get(config_name, None)
172
173
 
173
- def process_configs(self, ctx, param, value):
174
+ def process_configs(
175
+ self,
176
+ flow_name: str,
177
+ param_name: str,
178
+ param_value: Dict[str, Optional[str]],
179
+ quiet: bool,
180
+ datastore: str,
181
+ click_obj: Optional[Any] = None,
182
+ ):
174
183
  from ..cli import echo_always, echo_dev_null # Prevent circular import
175
184
  from ..flowspec import _FlowState # Prevent circular import
176
185
 
@@ -198,31 +207,32 @@ class ConfigInput:
198
207
  # - the default (including None if there is no default). If the default is
199
208
  # not None, it will start with _CONVERTED_DEFAULT since Click will make
200
209
  # the value go through ConvertPath or ConvertDictOrStr
201
- # - the actual value passed through prefixed with _CONVERT_PREFIX
210
+ # - the actual value passed through prefixed with _CONVERT_PREFIX
202
211
 
203
212
  debug.userconf_exec(
204
213
  "Processing configs for %s -- incoming values: %s"
205
- % (param.name, str(value))
214
+ % (param_name, str(param_value))
206
215
  )
207
216
 
208
217
  do_return = self._value_values is None and self._path_values is None
209
218
  # We only keep around non default values. We could simplify by checking just one
210
219
  # value and if it is default it means all are but this doesn't seem much more effort
211
220
  # and is clearer
212
- if param.name == "config_value_options":
221
+ if param_name == "config_value":
213
222
  self._value_values = {
214
223
  k.lower(): v
215
- for k, v in value
224
+ for k, v in param_value
216
225
  if v is not None and not v.startswith(_CONVERTED_DEFAULT)
217
226
  }
218
227
  else:
219
228
  self._path_values = {
220
229
  k.lower(): v
221
- for k, v in value
230
+ for k, v in param_value
222
231
  if v is not None and not v.startswith(_CONVERTED_DEFAULT)
223
232
  }
224
233
  if do_return:
225
- # One of config_value_options or config_file_options will be None
234
+ # One of values["value"] or values["path"] is None -- we are in the first
235
+ # go around
226
236
  debug.userconf_exec("Incomplete config options; waiting for more")
227
237
  return None
228
238
 
@@ -233,8 +243,13 @@ class ConfigInput:
233
243
  # down configurations (like METAFLOW_FLOW_CONFIG) could still be present and
234
244
  # would cause an issue -- we can ignore those as the kv. values should trump
235
245
  # everything else.
246
+ # NOTE: These are all *non default* keys
236
247
  all_keys = set(self._value_values).union(self._path_values)
237
- # Make sure we have at least some keys (ie: some non default values)
248
+
249
+ if all_keys and click_obj:
250
+ click_obj.has_cl_config_options = True
251
+ # Make sure we have at least some non default keys (we need some if we have
252
+ # all kv)
238
253
  has_all_kv = all_keys and all(
239
254
  self._value_values.get(k, "").startswith(_CONVERT_PREFIX + "kv.")
240
255
  for k in all_keys
@@ -244,27 +259,35 @@ class ConfigInput:
244
259
  to_return = {}
245
260
 
246
261
  if not has_all_kv:
247
- # Check that the user didn't provide *both* a path and a value.
262
+ # Check that the user didn't provide *both* a path and a value. Again, these
263
+ # are only user-provided (not defaults)
248
264
  common_keys = set(self._value_values or []).intersection(
249
265
  [k for k, v in self._path_values.items()] or []
250
266
  )
251
267
  if common_keys:
252
- raise click.UsageError(
268
+ exc = click.UsageError(
253
269
  "Cannot provide both a value and a file for the same configuration. "
254
270
  "Found such values for '%s'" % "', '".join(common_keys)
255
271
  )
272
+ if click_obj:
273
+ click_obj.delayed_config_exception = exc
274
+ return None
275
+ raise exc
256
276
 
257
- all_values = dict(self._path_values or {})
258
- all_values.update(self._value_values or {})
277
+ all_values = dict(self._path_values)
278
+ all_values.update(self._value_values)
259
279
 
260
280
  debug.userconf_exec("All config values: %s" % str(all_values))
261
281
 
262
282
  merged_configs = {}
283
+ # Now look at everything (including defaults)
263
284
  for name, (val, is_path) in self._defaults.items():
264
285
  n = name.lower()
265
286
  if n in all_values:
287
+ # We have the value provided by the user -- use that.
266
288
  merged_configs[n] = all_values[n]
267
289
  else:
290
+ # No value provided by the user -- use the default
268
291
  if isinstance(val, DeployTimeField):
269
292
  # This supports a default value that is a deploy-time field (similar
270
293
  # to Parameter).)
@@ -273,25 +296,27 @@ class ConfigInput:
273
296
  # of circularity. Note also that quiet and datastore are *eager*
274
297
  # options so are available here.
275
298
  param_ctx = ParameterContext(
276
- flow_name=ctx.obj.flow.name,
299
+ flow_name=flow_name,
277
300
  user_name=get_username(),
278
301
  parameter_name=n,
279
- logger=(
280
- echo_dev_null if ctx.params["quiet"] else echo_always
281
- ),
282
- ds_type=ctx.params["datastore"],
302
+ logger=(echo_dev_null if quiet else echo_always),
303
+ ds_type=datastore,
283
304
  configs=None,
284
305
  )
285
306
  val = val.fun(param_ctx)
286
307
  if is_path:
287
308
  # This is a file path
288
- merged_configs[n] = ConvertPath.convert_value(val, False)
309
+ merged_configs[n] = ConvertPath.convert_value(val, True)
289
310
  else:
290
311
  # This is a value
291
- merged_configs[n] = ConvertDictOrStr.convert_value(val, False)
312
+ merged_configs[n] = ConvertDictOrStr.convert_value(val, True)
292
313
  else:
293
314
  debug.userconf_exec("Fast path due to pre-processed values")
294
315
  merged_configs = self._value_values
316
+
317
+ if click_obj:
318
+ click_obj.has_config_options = True
319
+
295
320
  debug.userconf_exec("Configs merged with defaults: %s" % str(merged_configs))
296
321
 
297
322
  missing_configs = set()
@@ -302,20 +327,27 @@ class ConfigInput:
302
327
  if val is None:
303
328
  missing_configs.add(name)
304
329
  continue
305
- if val.startswith(_CONVERTED_NO_FILE):
306
- no_file.append(name)
307
- continue
308
330
  if val.startswith(_CONVERTED_DEFAULT_NO_FILE):
309
331
  no_default_file.append(name)
310
332
  continue
333
+ if val.startswith(_CONVERTED_NO_FILE):
334
+ no_file.append(name)
335
+ continue
336
+
311
337
  val = val[len(_CONVERT_PREFIX) :] # Remove the _CONVERT_PREFIX
338
+ if val.startswith(_DEFAULT_PREFIX): # Remove the _DEFAULT_PREFIX if needed
339
+ val = val[len(_DEFAULT_PREFIX) :]
312
340
  if val.startswith("kv."):
313
341
  # This means to load it from a file
314
342
  read_value = self.get_config(val[3:])
315
343
  if read_value is None:
316
- raise click.UsageError(
344
+ exc = click.UsageError(
317
345
  "Could not find configuration '%s' in INFO file" % val
318
346
  )
347
+ if click_obj:
348
+ click_obj.delayed_config_exception = exc
349
+ return None
350
+ raise exc
319
351
  flow_cls._flow_state[_FlowState.CONFIGS][name] = read_value
320
352
  to_return[name] = ConfigValue(read_value)
321
353
  else:
@@ -348,13 +380,27 @@ class ConfigInput:
348
380
  % (merged_configs[missing][len(_CONVERTED_DEFAULT_NO_FILE) :], missing)
349
381
  )
350
382
  if msgs:
351
- raise click.UsageError(
383
+ exc = click.UsageError(
352
384
  "Bad values passed for configuration options: %s" % ", ".join(msgs)
353
385
  )
386
+ if click_obj:
387
+ click_obj.delayed_config_exception = exc
388
+ return None
389
+ raise exc
354
390
 
355
391
  debug.userconf_exec("Finalized configs: %s" % str(to_return))
356
392
  return to_return
357
393
 
394
+ def process_configs_click(self, ctx, param, value):
395
+ return self.process_configs(
396
+ ctx.obj.flow.name,
397
+ param.name,
398
+ value,
399
+ ctx.params["quiet"],
400
+ ctx.params["datastore"],
401
+ click_obj=ctx.obj,
402
+ )
403
+
358
404
  def __str__(self):
359
405
  return repr(self)
360
406
 
@@ -399,7 +445,7 @@ class LocalFileInput(click.Path):
399
445
  return "LocalFileInput"
400
446
 
401
447
 
402
- def config_options(cmd):
448
+ def config_options_with_config_input(cmd):
403
449
  help_strs = []
404
450
  required_names = []
405
451
  defaults = {}
@@ -407,7 +453,7 @@ def config_options(cmd):
407
453
  parsers = {}
408
454
  flow_cls = getattr(current_flow, "flow_cls", None)
409
455
  if flow_cls is None:
410
- return cmd
456
+ return cmd, None
411
457
 
412
458
  parameters = [p for _, p in flow_cls._get_parameters() if p.IS_CONFIG_PARAMETER]
413
459
  # List all the configuration options
@@ -432,20 +478,22 @@ def config_options(cmd):
432
478
  parsers[arg.name.lower()] = arg.parser
433
479
 
434
480
  if not config_seen:
435
- # No configurations -- don't add anything
436
- return cmd
481
+ # No configurations -- don't add anything; we set it to False so that it
482
+ # can be checked whether or not we called this.
483
+ return cmd, False
437
484
 
438
485
  help_str = (
439
486
  "Configuration options for the flow. "
440
- "Multiple configurations can be specified."
487
+ "Multiple configurations can be specified. Cannot be used with resume."
441
488
  )
442
489
  help_str = "\n\n".join([help_str] + help_strs)
443
- cb_func = ConfigInput(required_names, defaults, parsers).process_configs
490
+ config_input = ConfigInput(required_names, defaults, parsers)
491
+ cb_func = config_input.process_configs_click
444
492
 
445
493
  cmd.params.insert(
446
494
  0,
447
495
  click.Option(
448
- ["--config-value", "config_value_options"],
496
+ ["--config-value", "config_value"],
449
497
  nargs=2,
450
498
  multiple=True,
451
499
  type=MultipleTuple([click.Choice(config_seen), ConvertDictOrStr()]),
@@ -470,7 +518,7 @@ def config_options(cmd):
470
518
  cmd.params.insert(
471
519
  0,
472
520
  click.Option(
473
- ["--config", "config_file_options"],
521
+ ["--config", "config_file"],
474
522
  nargs=2,
475
523
  multiple=True,
476
524
  type=MultipleTuple([click.Choice(config_seen), ConvertPath()]),
@@ -492,4 +540,9 @@ def config_options(cmd):
492
540
  required=False,
493
541
  ),
494
542
  )
543
+ return cmd, config_input
544
+
545
+
546
+ def config_options(cmd):
547
+ cmd, _ = config_options_with_config_input(cmd)
495
548
  return cmd