metaflow 2.12.36__py2.py3-none-any.whl → 2.12.38__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 (45) hide show
  1. metaflow/__init__.py +3 -0
  2. metaflow/cli.py +84 -697
  3. metaflow/cli_args.py +17 -0
  4. metaflow/cli_components/__init__.py +0 -0
  5. metaflow/cli_components/dump_cmd.py +96 -0
  6. metaflow/cli_components/init_cmd.py +51 -0
  7. metaflow/cli_components/run_cmds.py +358 -0
  8. metaflow/cli_components/step_cmd.py +189 -0
  9. metaflow/cli_components/utils.py +140 -0
  10. metaflow/cmd/develop/stub_generator.py +9 -2
  11. metaflow/decorators.py +63 -2
  12. metaflow/extension_support/plugins.py +41 -27
  13. metaflow/flowspec.py +156 -16
  14. metaflow/includefile.py +50 -22
  15. metaflow/metaflow_config.py +1 -1
  16. metaflow/package.py +17 -3
  17. metaflow/parameters.py +80 -23
  18. metaflow/plugins/__init__.py +4 -0
  19. metaflow/plugins/airflow/airflow_cli.py +1 -0
  20. metaflow/plugins/argo/argo_workflows.py +41 -1
  21. metaflow/plugins/argo/argo_workflows_cli.py +1 -0
  22. metaflow/plugins/aws/batch/batch_decorator.py +2 -2
  23. metaflow/plugins/aws/step_functions/step_functions.py +32 -0
  24. metaflow/plugins/aws/step_functions/step_functions_cli.py +1 -0
  25. metaflow/plugins/datatools/s3/s3op.py +3 -3
  26. metaflow/plugins/kubernetes/kubernetes_cli.py +1 -1
  27. metaflow/plugins/kubernetes/kubernetes_decorator.py +2 -2
  28. metaflow/plugins/pypi/conda_decorator.py +22 -0
  29. metaflow/plugins/pypi/pypi_decorator.py +1 -0
  30. metaflow/plugins/timeout_decorator.py +2 -2
  31. metaflow/runner/click_api.py +73 -19
  32. metaflow/runtime.py +111 -73
  33. metaflow/sidecar/sidecar_worker.py +1 -1
  34. metaflow/user_configs/__init__.py +0 -0
  35. metaflow/user_configs/config_decorators.py +563 -0
  36. metaflow/user_configs/config_options.py +495 -0
  37. metaflow/user_configs/config_parameters.py +386 -0
  38. metaflow/util.py +17 -0
  39. metaflow/version.py +1 -1
  40. {metaflow-2.12.36.dist-info → metaflow-2.12.38.dist-info}/METADATA +3 -2
  41. {metaflow-2.12.36.dist-info → metaflow-2.12.38.dist-info}/RECORD +45 -35
  42. {metaflow-2.12.36.dist-info → metaflow-2.12.38.dist-info}/LICENSE +0 -0
  43. {metaflow-2.12.36.dist-info → metaflow-2.12.38.dist-info}/WHEEL +0 -0
  44. {metaflow-2.12.36.dist-info → metaflow-2.12.38.dist-info}/entry_points.txt +0 -0
  45. {metaflow-2.12.36.dist-info → metaflow-2.12.38.dist-info}/top_level.txt +0 -0
metaflow/flowspec.py CHANGED
@@ -4,15 +4,18 @@ import sys
4
4
  import traceback
5
5
  import reprlib
6
6
 
7
+ from enum import Enum
7
8
  from itertools import islice
8
9
  from types import FunctionType, MethodType
9
- from typing import Any, Callable, List, Optional, Tuple
10
+ from typing import TYPE_CHECKING, Any, Callable, Generator, List, Optional, Tuple
10
11
 
11
12
  from . import cmd_with_io, parameters
13
+ from .debug import debug
12
14
  from .parameters import DelayedEvaluationParameter, Parameter
13
15
  from .exception import (
14
16
  MetaflowException,
15
17
  MissingInMergeArtifactsException,
18
+ MetaflowInternalError,
16
19
  UnhandledInMergeArtifactsException,
17
20
  )
18
21
 
@@ -20,6 +23,12 @@ from .extension_support import extension_info
20
23
 
21
24
  from .graph import FlowGraph
22
25
  from .unbounded_foreach import UnboundedForeachInput
26
+ from .user_configs.config_decorators import (
27
+ CustomFlowDecorator,
28
+ CustomStepDecorator,
29
+ MutableFlow,
30
+ MutableStep,
31
+ )
23
32
  from .util import to_pod
24
33
  from .metaflow_config import INCLUDE_FOREACH_STACK, MAXIMUM_FOREACH_VALUE_CHARS
25
34
 
@@ -65,14 +74,27 @@ class ParallelUBF(UnboundedForeachInput):
65
74
  return item or 0 # item is None for the control task, but it is also split 0
66
75
 
67
76
 
77
+ class _FlowState(Enum):
78
+ CONFIGS = 1
79
+ CONFIG_DECORATORS = 2
80
+ CACHED_PARAMETERS = 3
81
+
82
+
68
83
  class FlowSpecMeta(type):
69
84
  def __new__(cls, name, bases, dct):
70
85
  f = super().__new__(cls, name, bases, dct)
71
- # This makes sure to give _flow_decorators to each
72
- # child class (and not share it with the FlowSpec base
73
- # class). This is important to not make a "global"
74
- # _flow_decorators
86
+ # We store some state in the flow class itself. This is primarily used to
87
+ # attach global state to a flow. It is *not* an actual global because of
88
+ # Runner/NBRunner. This is also created here in the meta class to avoid it being
89
+ # shared between different children classes.
90
+
91
+ # We should move _flow_decorators into this structure as well but keeping it
92
+ # out to limit the changes for now.
75
93
  f._flow_decorators = {}
94
+
95
+ # Keys are _FlowState enum values
96
+ f._flow_state = {}
97
+
76
98
  return f
77
99
 
78
100
 
@@ -96,6 +118,7 @@ class FlowSpec(metaclass=FlowSpecMeta):
96
118
  "_cached_input",
97
119
  "_graph",
98
120
  "_flow_decorators",
121
+ "_flow_state",
99
122
  "_steps",
100
123
  "index",
101
124
  "input",
@@ -148,16 +171,11 @@ class FlowSpec(metaclass=FlowSpecMeta):
148
171
  fname = fname[:-1]
149
172
  return os.path.basename(fname)
150
173
 
151
- def _set_constants(self, graph, kwargs):
152
- from metaflow.decorators import (
153
- flow_decorators,
154
- ) # To prevent circular dependency
155
-
156
- # Persist values for parameters and other constants (class level variables)
157
- # only once. This method is called before persist_constants is called to
158
- # persist all values set using setattr
174
+ def _check_parameters(self, config_parameters=False):
159
175
  seen = set()
160
- for var, param in self._get_parameters():
176
+ for _, param in self._get_parameters():
177
+ if param.IS_CONFIG_PARAMETER != config_parameters:
178
+ continue
161
179
  norm = param.name.lower()
162
180
  if norm in seen:
163
181
  raise MetaflowException(
@@ -166,13 +184,127 @@ class FlowSpec(metaclass=FlowSpecMeta):
166
184
  "case-insensitive." % param.name
167
185
  )
168
186
  seen.add(norm)
169
- seen.clear()
187
+
188
+ def _process_config_decorators(self, config_options):
189
+ current_cls = self.__class__
190
+
191
+ # Fast path for no user configurations
192
+ if not self._flow_state.get(_FlowState.CONFIG_DECORATORS):
193
+ # Process parameters to allow them to also use config values easily
194
+ for var, param in self._get_parameters():
195
+ if param.IS_CONFIG_PARAMETER:
196
+ continue
197
+ param.init()
198
+ return self
199
+
200
+ debug.userconf_exec("Processing mutating step/flow decorators")
201
+ # We need to convert all the user configurations from DelayedEvaluationParameters
202
+ # to actual values so they can be used as is in the config decorators.
203
+
204
+ # We then reset them to be proper configs so they can be re-evaluated in
205
+ # _set_constants
206
+ to_reset_configs = []
207
+ self._check_parameters(config_parameters=True)
208
+ for var, param in self._get_parameters():
209
+ if not param.IS_CONFIG_PARAMETER:
210
+ continue
211
+ # Note that a config with no default and not required will be None
212
+ val = config_options.get(param.name.replace("-", "_").lower())
213
+ if isinstance(val, DelayedEvaluationParameter):
214
+ val = val()
215
+ # We store the value as well so that in _set_constants, we don't try
216
+ # to recompute (no guarantee that it is stable)
217
+ param._store_value(val)
218
+ to_reset_configs.append((var, param))
219
+ debug.userconf_exec("Setting config %s to %s" % (var, str(val)))
220
+ setattr(current_cls, var, val)
221
+
222
+ # Run all the decorators. Step decorators are directly in the step and
223
+ # we will run those first and *then* we run all the flow level decorators
224
+ for step in self._steps:
225
+ for deco in step.config_decorators:
226
+ if isinstance(deco, CustomStepDecorator):
227
+ debug.userconf_exec(
228
+ "Evaluating step level decorator %s for %s"
229
+ % (deco.__class__.__name__, step.name)
230
+ )
231
+ deco.evaluate(MutableStep(current_cls, step))
232
+ else:
233
+ raise MetaflowInternalError(
234
+ "A non CustomFlowDecorator found in step custom decorators"
235
+ )
236
+ if step.config_decorators:
237
+ # We remove all mention of the custom step decorator
238
+ setattr(current_cls, step.name, step)
239
+
240
+ mutable_flow = MutableFlow(current_cls)
241
+ for deco in self._flow_state[_FlowState.CONFIG_DECORATORS]:
242
+ if isinstance(deco, CustomFlowDecorator):
243
+ # Sanity check to make sure we are applying the decorator to the right
244
+ # class
245
+ if not deco._flow_cls == current_cls and not issubclass(
246
+ current_cls, deco._flow_cls
247
+ ):
248
+ raise MetaflowInternalError(
249
+ "CustomFlowDecorator registered on the wrong flow -- "
250
+ "expected %s but got %s"
251
+ % (deco._flow_cls.__name__, current_cls.__name__)
252
+ )
253
+ debug.userconf_exec(
254
+ "Evaluating flow level decorator %s" % deco.__class__.__name__
255
+ )
256
+ deco.evaluate(mutable_flow)
257
+ # We reset cached_parameters on the very off chance that the user added
258
+ # more configurations based on the configuration
259
+ if _FlowState.CACHED_PARAMETERS in current_cls._flow_state:
260
+ del current_cls._flow_state[_FlowState.CACHED_PARAMETERS]
261
+ else:
262
+ raise MetaflowInternalError(
263
+ "A non CustomFlowDecorator found in flow custom decorators"
264
+ )
265
+
266
+ # Process parameters to allow them to also use config values easily
267
+ for var, param in self._get_parameters():
268
+ if param.IS_CONFIG_PARAMETER:
269
+ continue
270
+ param.init()
271
+ # Reset all configs that were already present in the class.
272
+ # TODO: This means that users can't override configs directly. Not sure if this
273
+ # is a pattern we want to support
274
+ for var, param in to_reset_configs:
275
+ setattr(current_cls, var, param)
276
+
277
+ # Reset cached parameters again since we added back the config parameters
278
+ if _FlowState.CACHED_PARAMETERS in current_cls._flow_state:
279
+ del current_cls._flow_state[_FlowState.CACHED_PARAMETERS]
280
+
281
+ # Set the current flow class we are in (the one we just created)
282
+ parameters.replace_flow_context(current_cls)
283
+ return current_cls(use_cli=False)
284
+
285
+ def _set_constants(self, graph, kwargs, config_options):
286
+ from metaflow.decorators import (
287
+ flow_decorators,
288
+ ) # To prevent circular dependency
289
+
290
+ # Persist values for parameters and other constants (class level variables)
291
+ # only once. This method is called before persist_constants is called to
292
+ # persist all values set using setattr
293
+ self._check_parameters(config_parameters=False)
294
+
295
+ seen = set()
170
296
  self._success = True
171
297
 
172
298
  parameters_info = []
173
299
  for var, param in self._get_parameters():
174
300
  seen.add(var)
175
- val = kwargs[param.name.replace("-", "_").lower()]
301
+ if param.IS_CONFIG_PARAMETER:
302
+ # Use computed value if already evaluated, else get from config_options
303
+ val = param._computed_value or config_options.get(
304
+ param.name.replace("-", "_").lower()
305
+ )
306
+ else:
307
+ val = kwargs[param.name.replace("-", "_").lower()]
176
308
  # Support for delayed evaluation of parameters.
177
309
  if isinstance(val, DelayedEvaluationParameter):
178
310
  val = val()
@@ -218,6 +350,12 @@ class FlowSpec(metaclass=FlowSpecMeta):
218
350
 
219
351
  @classmethod
220
352
  def _get_parameters(cls):
353
+ cached = cls._flow_state.get(_FlowState.CACHED_PARAMETERS)
354
+ if cached is not None:
355
+ for var in cached:
356
+ yield var, getattr(cls, var)
357
+ return
358
+ build_list = []
221
359
  for var in dir(cls):
222
360
  if var[0] == "_" or var in cls._NON_PARAMETERS:
223
361
  continue
@@ -226,7 +364,9 @@ class FlowSpec(metaclass=FlowSpecMeta):
226
364
  except:
227
365
  continue
228
366
  if isinstance(val, Parameter):
367
+ build_list.append(var)
229
368
  yield var, val
369
+ cls._flow_state[_FlowState.CACHED_PARAMETERS] = build_list
230
370
 
231
371
  def _set_datastore(self, datastore):
232
372
  self._datastore = datastore
metaflow/includefile.py CHANGED
@@ -20,6 +20,7 @@ from .parameters import (
20
20
  )
21
21
 
22
22
  from .plugins import DATACLIENTS
23
+ from .user_configs.config_parameters import ConfigValue
23
24
  from .util import get_username
24
25
 
25
26
  import functools
@@ -136,6 +137,7 @@ class FilePathClass(click.ParamType):
136
137
  parameter_name=param.name,
137
138
  logger=ctx.obj.echo,
138
139
  ds_type=ctx.obj.datastore_impl.TYPE,
140
+ configs=None,
139
141
  )
140
142
 
141
143
  if len(value) > 0 and (value.startswith("{") or value.startswith('"{')):
@@ -243,29 +245,63 @@ class IncludeFile(Parameter):
243
245
  default : Union[str, Callable[ParameterContext, str]]
244
246
  Default path to a local file. A function
245
247
  implies that the parameter corresponds to a *deploy-time parameter*.
246
- is_text : bool, default True
248
+ is_text : bool, optional, default None
247
249
  Convert the file contents to a string using the provided `encoding`.
248
- If False, the artifact is stored in `bytes`.
249
- encoding : str, optional, default 'utf-8'
250
- Use this encoding to decode the file contexts if `is_text=True`.
251
- required : bool, default False
250
+ If False, the artifact is stored in `bytes`. A value of None is equivalent to
251
+ True.
252
+ encoding : str, optional, default None
253
+ Use this encoding to decode the file contexts if `is_text=True`. A value of None
254
+ is equivalent to "utf-8".
255
+ required : bool, optional, default None
252
256
  Require that the user specified a value for the parameter.
253
- `required=True` implies that the `default` is not used.
257
+ `required=True` implies that the `default` is not used. A value of None is
258
+ equivalent to False
254
259
  help : str, optional
255
260
  Help text to show in `run --help`.
256
261
  show_default : bool, default True
257
- If True, show the default value in the help text.
262
+ If True, show the default value in the help text. A value of None is equivalent
263
+ to True.
258
264
  """
259
265
 
260
266
  def __init__(
261
267
  self,
262
268
  name: str,
263
- required: bool = False,
264
- is_text: bool = True,
265
- encoding: str = "utf-8",
269
+ required: Optional[bool] = None,
270
+ is_text: Optional[bool] = None,
271
+ encoding: Optional[str] = None,
266
272
  help: Optional[str] = None,
267
273
  **kwargs: Dict[str, str]
268
274
  ):
275
+ self._includefile_overrides = {}
276
+ if is_text is not None:
277
+ self._includefile_overrides["is_text"] = is_text
278
+ if encoding is not None:
279
+ self._includefile_overrides["encoding"] = encoding
280
+ # NOTA: Right now, there is an issue where these can't be overridden by config
281
+ # in all circumstances. Ignoring for now.
282
+ super(IncludeFile, self).__init__(
283
+ name,
284
+ required=required,
285
+ help=help,
286
+ type=FilePathClass(
287
+ self._includefile_overrides.get("is_text", True),
288
+ self._includefile_overrides.get("encoding", "utf-8"),
289
+ ),
290
+ **kwargs,
291
+ )
292
+
293
+ def init(self):
294
+ super(IncludeFile, self).init()
295
+
296
+ # This will use the values set explicitly in the args if present, else will
297
+ # use and remove from kwargs else will use True/utf-8
298
+ is_text = self._includefile_overrides.get(
299
+ "is_text", self.kwargs.pop("is_text", True)
300
+ )
301
+ encoding = self._includefile_overrides.get(
302
+ "encoding", self.kwargs.pop("encoding", "utf-8")
303
+ )
304
+
269
305
  # If a default is specified, it needs to be uploaded when the flow is deployed
270
306
  # (for example when doing a `step-functions create`) so we make the default
271
307
  # be a DeployTimeField. This means that it will be evaluated in two cases:
@@ -275,7 +311,7 @@ class IncludeFile(Parameter):
275
311
  # In the first case, we will need to fully upload the file whereas in the
276
312
  # second case, we can just return the string as the FilePath.convert method
277
313
  # will take care of evaluating things.
278
- v = kwargs.get("default")
314
+ v = self.kwargs.get("default")
279
315
  if v is not None:
280
316
  # If the default is a callable, we have two DeployTimeField:
281
317
  # - the callable nature of the default will require us to "call" the default
@@ -288,23 +324,15 @@ class IncludeFile(Parameter):
288
324
  # (call the default)
289
325
  if callable(v) and not isinstance(v, DeployTimeField):
290
326
  # If default is a callable, make it a DeployTimeField (the inner one)
291
- v = DeployTimeField(name, str, "default", v, return_str=True)
292
- kwargs["default"] = DeployTimeField(
293
- name,
327
+ v = DeployTimeField(self.name, str, "default", v, return_str=True)
328
+ self.kwargs["default"] = DeployTimeField(
329
+ self.name,
294
330
  str,
295
331
  "default",
296
332
  IncludeFile._eval_default(is_text, encoding, v),
297
333
  print_representation=v,
298
334
  )
299
335
 
300
- super(IncludeFile, self).__init__(
301
- name,
302
- required=required,
303
- help=help,
304
- type=FilePathClass(is_text, encoding),
305
- **kwargs,
306
- )
307
-
308
336
  def load_parameter(self, v):
309
337
  if v is None:
310
338
  return v
@@ -440,7 +440,7 @@ ESCAPE_HATCH_WARNING = from_conf("ESCAPE_HATCH_WARNING", True)
440
440
  ###
441
441
  # Debug configuration
442
442
  ###
443
- DEBUG_OPTIONS = ["subcommand", "sidecar", "s3client", "tracing", "stubgen"]
443
+ DEBUG_OPTIONS = ["subcommand", "sidecar", "s3client", "tracing", "stubgen", "userconf"]
444
444
 
445
445
  for typ in DEBUG_OPTIONS:
446
446
  vars()["DEBUG_%s" % typ.upper()] = from_conf("DEBUG_%s" % typ.upper(), False)
metaflow/package.py CHANGED
@@ -6,6 +6,7 @@ import time
6
6
  import json
7
7
  from io import BytesIO
8
8
 
9
+ from .user_configs.config_parameters import CONFIG_FILE, dump_config_values
9
10
  from .extension_support import EXT_PKG, package_mfext_all
10
11
  from .metaflow_config import DEFAULT_PACKAGE_SUFFIXES
11
12
  from .exception import MetaflowException
@@ -151,11 +152,23 @@ class MetaflowPackage(object):
151
152
  for path_tuple in self._walk(flowdir, suffixes=self.suffixes):
152
153
  yield path_tuple
153
154
 
155
+ def _add_configs(self, tar):
156
+ buf = BytesIO()
157
+ buf.write(json.dumps(dump_config_values(self._flow)).encode("utf-8"))
158
+ self._add_file(tar, os.path.basename(CONFIG_FILE), buf)
159
+
154
160
  def _add_info(self, tar):
155
- info = tarfile.TarInfo(os.path.basename(INFO_FILE))
156
- env = self.environment.get_environment_info(include_ext_info=True)
157
161
  buf = BytesIO()
158
- buf.write(json.dumps(env).encode("utf-8"))
162
+ buf.write(
163
+ json.dumps(
164
+ self.environment.get_environment_info(include_ext_info=True)
165
+ ).encode("utf-8")
166
+ )
167
+ self._add_file(tar, os.path.basename(INFO_FILE), buf)
168
+
169
+ @staticmethod
170
+ def _add_file(tar, filename, buf):
171
+ info = tarfile.TarInfo(filename)
159
172
  buf.seek(0)
160
173
  info.size = len(buf.getvalue())
161
174
  # Setting this default to Dec 3, 2019
@@ -175,6 +188,7 @@ class MetaflowPackage(object):
175
188
  fileobj=buf, mode="w:gz", compresslevel=3, dereference=True
176
189
  ) as tar:
177
190
  self._add_info(tar)
191
+ self._add_configs(tar)
178
192
  for path, arcname in self.path_tuples():
179
193
  tar.add(path, arcname=arcname, recursive=False, filter=no_mtime)
180
194
 
metaflow/parameters.py CHANGED
@@ -3,7 +3,7 @@ import json
3
3
  from contextlib import contextmanager
4
4
  from threading import local
5
5
 
6
- from typing import Any, Callable, Dict, NamedTuple, Optional, Type, Union
6
+ from typing import Any, Callable, Dict, NamedTuple, Optional, TYPE_CHECKING, Type, Union
7
7
 
8
8
  from metaflow._vendor import click
9
9
 
@@ -14,6 +14,9 @@ from .exception import (
14
14
  MetaflowException,
15
15
  )
16
16
 
17
+ if TYPE_CHECKING:
18
+ from .user_configs.config_parameters import ConfigValue
19
+
17
20
  try:
18
21
  # Python2
19
22
  strtype = basestring
@@ -32,6 +35,7 @@ ParameterContext = NamedTuple(
32
35
  ("parameter_name", str),
33
36
  ("logger", Callable[..., None]),
34
37
  ("ds_type", str),
38
+ ("configs", Optional["ConfigValue"]),
35
39
  ],
36
40
  )
37
41
 
@@ -72,6 +76,16 @@ def flow_context(flow_cls):
72
76
  context_proto = None
73
77
 
74
78
 
79
+ def replace_flow_context(flow_cls):
80
+ """
81
+ Replace the current flow context with a new flow class. This is used
82
+ when we change the current flow class after having run user configuration functions
83
+ """
84
+ current_flow.flow_cls_stack = current_flow.flow_cls_stack[1:]
85
+ current_flow.flow_cls_stack.insert(0, flow_cls)
86
+ current_flow.flow_cls = current_flow.flow_cls_stack[0]
87
+
88
+
75
89
  class JSONTypeClass(click.ParamType):
76
90
  name = "JSON"
77
91
 
@@ -210,12 +224,18 @@ class DeployTimeField(object):
210
224
  def deploy_time_eval(value):
211
225
  if isinstance(value, DeployTimeField):
212
226
  return value(deploy_time=True)
227
+ elif isinstance(value, DelayedEvaluationParameter):
228
+ return value(return_str=True)
213
229
  else:
214
230
  return value
215
231
 
216
232
 
217
233
  # this is called by cli.main
218
- def set_parameter_context(flow_name, echo, datastore):
234
+ def set_parameter_context(flow_name, echo, datastore, configs):
235
+ from .user_configs.config_parameters import (
236
+ ConfigValue,
237
+ ) # Prevent circular dependency
238
+
219
239
  global context_proto
220
240
  context_proto = ParameterContext(
221
241
  flow_name=flow_name,
@@ -223,6 +243,7 @@ def set_parameter_context(flow_name, echo, datastore):
223
243
  parameter_name=None,
224
244
  logger=echo,
225
245
  ds_type=datastore.TYPE,
246
+ configs=ConfigValue(dict(configs)),
226
247
  )
227
248
 
228
249
 
@@ -279,7 +300,11 @@ class Parameter(object):
279
300
  ----------
280
301
  name : str
281
302
  User-visible parameter name.
282
- default : str or float or int or bool or `JSONType` or a function.
303
+ default : Union[str, float, int, bool, Dict[str, Any],
304
+ Callable[
305
+ [ParameterContext], Union[str, float, int, bool, Dict[str, Any]]
306
+ ],
307
+ ], optional, default None
283
308
  Default value for the parameter. Use a special `JSONType` class to
284
309
  indicate that the value must be a valid JSON object. A function
285
310
  implies that the parameter corresponds to a *deploy-time parameter*.
@@ -288,15 +313,19 @@ class Parameter(object):
288
313
  If `default` is not specified, define the parameter type. Specify
289
314
  one of `str`, `float`, `int`, `bool`, or `JSONType`. If None, defaults
290
315
  to the type of `default` or `str` if none specified.
291
- help : str, optional
316
+ help : str, optional, default None
292
317
  Help text to show in `run --help`.
293
- required : bool, default False
294
- Require that the user specified a value for the parameter.
295
- `required=True` implies that the `default` is not used.
296
- show_default : bool, default True
297
- If True, show the default value in the help text.
318
+ required : bool, optional, default None
319
+ Require that the user specified a value for the parameter. Note that if
320
+ a default is provide, the required flag is ignored.
321
+ A value of None is equivalent to False.
322
+ show_default : bool, optional, default None
323
+ If True, show the default value in the help text. A value of None is equivalent
324
+ to True.
298
325
  """
299
326
 
327
+ IS_CONFIG_PARAMETER = False
328
+
300
329
  def __init__(
301
330
  self,
302
331
  name: str,
@@ -307,31 +336,53 @@ class Parameter(object):
307
336
  int,
308
337
  bool,
309
338
  Dict[str, Any],
310
- Callable[[], Union[str, float, int, bool, Dict[str, Any]]],
339
+ Callable[
340
+ [ParameterContext], Union[str, float, int, bool, Dict[str, Any]]
341
+ ],
311
342
  ]
312
343
  ] = None,
313
344
  type: Optional[
314
345
  Union[Type[str], Type[float], Type[int], Type[bool], JSONTypeClass]
315
346
  ] = None,
316
347
  help: Optional[str] = None,
317
- required: bool = False,
318
- show_default: bool = True,
348
+ required: Optional[bool] = None,
349
+ show_default: Optional[bool] = None,
319
350
  **kwargs: Dict[str, Any]
320
351
  ):
321
352
  self.name = name
322
353
  self.kwargs = kwargs
323
- for k, v in {
354
+ self._override_kwargs = {
324
355
  "default": default,
325
356
  "type": type,
326
357
  "help": help,
327
358
  "required": required,
328
359
  "show_default": show_default,
329
- }.items():
330
- if v is not None:
331
- self.kwargs[k] = v
360
+ }
361
+
362
+ def init(self):
363
+ # Prevent circular import
364
+ from .user_configs.config_parameters import (
365
+ resolve_delayed_evaluator,
366
+ unpack_delayed_evaluator,
367
+ )
368
+
369
+ # Resolve any value from configurations
370
+ self.kwargs = unpack_delayed_evaluator(self.kwargs)
371
+ self.kwargs = resolve_delayed_evaluator(self.kwargs)
372
+
373
+ # This was the behavior before configs: values specified in args would override
374
+ # stuff in kwargs which is what we implement here as well
375
+ for key, value in self._override_kwargs.items():
376
+ if value is not None:
377
+ self.kwargs[key] = value
378
+ # Set two default values if no-one specified them
379
+ self.kwargs.setdefault("required", False)
380
+ self.kwargs.setdefault("show_default", True)
381
+
382
+ # Continue processing kwargs free of any configuration values :)
332
383
 
333
384
  # TODO: check that the type is one of the supported types
334
- param_type = self.kwargs["type"] = self._get_type(kwargs)
385
+ param_type = self.kwargs["type"] = self._get_type(self.kwargs)
335
386
 
336
387
  reserved_params = [
337
388
  "params",
@@ -356,23 +407,27 @@ class Parameter(object):
356
407
  raise MetaflowException(
357
408
  "Parameter name '%s' is a reserved "
358
409
  "word. Please use a different "
359
- "name for your parameter." % (name)
410
+ "name for your parameter." % (self.name)
360
411
  )
361
412
 
362
413
  # make sure the user is not trying to pass a function in one of the
363
414
  # fields that don't support function-values yet
364
415
  for field in ("show_default", "separator", "required"):
365
- if callable(kwargs.get(field)):
416
+ if callable(self.kwargs.get(field)):
366
417
  raise MetaflowException(
367
418
  "Parameter *%s*: Field '%s' cannot "
368
- "have a function as its value" % (name, field)
419
+ "have a function as its value" % (self.name, field)
369
420
  )
370
421
 
371
422
  # default can be defined as a function
372
423
  default_field = self.kwargs.get("default")
373
424
  if callable(default_field) and not isinstance(default_field, DeployTimeField):
374
425
  self.kwargs["default"] = DeployTimeField(
375
- name, param_type, "default", self.kwargs["default"], return_str=True
426
+ self.name,
427
+ param_type,
428
+ "default",
429
+ self.kwargs["default"],
430
+ return_str=True,
376
431
  )
377
432
 
378
433
  # note that separator doesn't work with DeployTimeFields unless you
@@ -381,7 +436,7 @@ class Parameter(object):
381
436
  if self.separator and not self.is_string_type:
382
437
  raise MetaflowException(
383
438
  "Parameter *%s*: Separator is only allowed "
384
- "for string parameters." % name
439
+ "for string parameters." % self.name
385
440
  )
386
441
 
387
442
  def __repr__(self):
@@ -437,7 +492,9 @@ def add_custom_parameters(deploy_mode=False):
437
492
  flow_cls = getattr(current_flow, "flow_cls", None)
438
493
  if flow_cls is None:
439
494
  return cmd
440
- parameters = [p for _, p in flow_cls._get_parameters()]
495
+ parameters = [
496
+ p for _, p in flow_cls._get_parameters() if not p.IS_CONFIG_PARAMETER
497
+ ]
441
498
  for arg in parameters[::-1]:
442
499
  kwargs = arg.option_kwargs(deploy_mode)
443
500
  cmd.params.insert(0, click.Option(("--" + arg.name,), **kwargs))
@@ -164,6 +164,10 @@ def get_plugin_cli():
164
164
  return resolve_plugins("cli")
165
165
 
166
166
 
167
+ def get_plugin_cli_path():
168
+ return resolve_plugins("cli", path_only=True)
169
+
170
+
167
171
  STEP_DECORATORS = resolve_plugins("step_decorator")
168
172
  FLOW_DECORATORS = resolve_plugins("flow_decorator")
169
173
  ENVIRONMENTS = resolve_plugins("environment")
@@ -283,6 +283,7 @@ def make_flow(
283
283
  ):
284
284
  # Attach @kubernetes.
285
285
  decorators._attach_decorators(obj.flow, [KubernetesDecorator.name])
286
+ decorators._init(obj.flow)
286
287
 
287
288
  decorators._init_step_decorators(
288
289
  obj.flow, obj.graph, obj.environment, obj.flow_datastore, obj.logger