metaflow 2.15.21__py2.py3-none-any.whl → 2.16.1__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 (74) hide show
  1. metaflow/__init__.py +7 -1
  2. metaflow/cli.py +19 -1
  3. metaflow/cli_components/init_cmd.py +1 -0
  4. metaflow/cli_components/run_cmds.py +8 -2
  5. metaflow/client/core.py +22 -30
  6. metaflow/datastore/task_datastore.py +0 -1
  7. metaflow/debug.py +5 -0
  8. metaflow/decorators.py +236 -70
  9. metaflow/extension_support/__init__.py +15 -8
  10. metaflow/extension_support/_empty_file.py +2 -2
  11. metaflow/flowspec.py +92 -60
  12. metaflow/graph.py +24 -2
  13. metaflow/meta_files.py +13 -0
  14. metaflow/metadata_provider/metadata.py +7 -1
  15. metaflow/metaflow_config.py +5 -0
  16. metaflow/metaflow_environment.py +82 -25
  17. metaflow/metaflow_version.py +1 -1
  18. metaflow/package/__init__.py +664 -0
  19. metaflow/packaging_sys/__init__.py +870 -0
  20. metaflow/packaging_sys/backend.py +113 -0
  21. metaflow/packaging_sys/distribution_support.py +153 -0
  22. metaflow/packaging_sys/tar_backend.py +86 -0
  23. metaflow/packaging_sys/utils.py +91 -0
  24. metaflow/packaging_sys/v1.py +480 -0
  25. metaflow/plugins/airflow/airflow.py +5 -1
  26. metaflow/plugins/airflow/airflow_cli.py +16 -5
  27. metaflow/plugins/argo/argo_workflows.py +15 -4
  28. metaflow/plugins/argo/argo_workflows_cli.py +17 -4
  29. metaflow/plugins/aws/batch/batch.py +22 -3
  30. metaflow/plugins/aws/batch/batch_cli.py +3 -0
  31. metaflow/plugins/aws/batch/batch_decorator.py +13 -5
  32. metaflow/plugins/aws/step_functions/step_functions.py +4 -1
  33. metaflow/plugins/aws/step_functions/step_functions_cli.py +16 -4
  34. metaflow/plugins/cards/card_decorator.py +0 -5
  35. metaflow/plugins/kubernetes/kubernetes.py +8 -1
  36. metaflow/plugins/kubernetes/kubernetes_cli.py +3 -0
  37. metaflow/plugins/kubernetes/kubernetes_decorator.py +13 -5
  38. metaflow/plugins/package_cli.py +25 -23
  39. metaflow/plugins/parallel_decorator.py +4 -2
  40. metaflow/plugins/pypi/bootstrap.py +8 -2
  41. metaflow/plugins/pypi/conda_decorator.py +39 -82
  42. metaflow/plugins/pypi/conda_environment.py +6 -2
  43. metaflow/plugins/pypi/pypi_decorator.py +4 -4
  44. metaflow/plugins/test_unbounded_foreach_decorator.py +2 -2
  45. metaflow/plugins/timeout_decorator.py +0 -1
  46. metaflow/plugins/uv/bootstrap.py +12 -1
  47. metaflow/plugins/uv/uv_environment.py +4 -2
  48. metaflow/pylint_wrapper.py +5 -1
  49. metaflow/runner/click_api.py +5 -4
  50. metaflow/runner/subprocess_manager.py +14 -2
  51. metaflow/runtime.py +37 -11
  52. metaflow/task.py +92 -7
  53. metaflow/user_configs/config_options.py +13 -8
  54. metaflow/user_configs/config_parameters.py +0 -4
  55. metaflow/user_decorators/__init__.py +0 -0
  56. metaflow/user_decorators/common.py +144 -0
  57. metaflow/user_decorators/mutable_flow.py +499 -0
  58. metaflow/user_decorators/mutable_step.py +424 -0
  59. metaflow/user_decorators/user_flow_decorator.py +264 -0
  60. metaflow/user_decorators/user_step_decorator.py +712 -0
  61. metaflow/util.py +4 -1
  62. metaflow/version.py +1 -1
  63. {metaflow-2.15.21.dist-info → metaflow-2.16.1.dist-info}/METADATA +2 -2
  64. {metaflow-2.15.21.dist-info → metaflow-2.16.1.dist-info}/RECORD +71 -60
  65. metaflow/info_file.py +0 -25
  66. metaflow/package.py +0 -203
  67. metaflow/user_configs/config_decorators.py +0 -568
  68. {metaflow-2.15.21.data → metaflow-2.16.1.data}/data/share/metaflow/devtools/Makefile +0 -0
  69. {metaflow-2.15.21.data → metaflow-2.16.1.data}/data/share/metaflow/devtools/Tiltfile +0 -0
  70. {metaflow-2.15.21.data → metaflow-2.16.1.data}/data/share/metaflow/devtools/pick_services.sh +0 -0
  71. {metaflow-2.15.21.dist-info → metaflow-2.16.1.dist-info}/WHEEL +0 -0
  72. {metaflow-2.15.21.dist-info → metaflow-2.16.1.dist-info}/entry_points.txt +0 -0
  73. {metaflow-2.15.21.dist-info → metaflow-2.16.1.dist-info}/licenses/LICENSE +0 -0
  74. {metaflow-2.15.21.dist-info → metaflow-2.16.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,424 @@
1
+ from functools import partial
2
+ from typing import (
3
+ Any,
4
+ Callable,
5
+ Dict,
6
+ Generator,
7
+ List,
8
+ Optional,
9
+ TYPE_CHECKING,
10
+ Tuple,
11
+ Union,
12
+ )
13
+
14
+ from metaflow.debug import debug
15
+ from metaflow.exception import MetaflowException
16
+
17
+ from .user_step_decorator import StepMutator, UserStepDecoratorBase
18
+
19
+ if TYPE_CHECKING:
20
+ import metaflow.decorators
21
+ import metaflow.flowspec
22
+
23
+
24
+ class MutableStep:
25
+ IGNORE = 1
26
+ ERROR = 2
27
+ OVERRIDE = 3
28
+
29
+ def __init__(
30
+ self,
31
+ flow_spec: "metaflow.flowspec.FlowSpec",
32
+ step: Union[
33
+ Callable[["metaflow.decorators.FlowSpecDerived"], None],
34
+ Callable[["metaflow.decorators.FlowSpecDerived", Any], None],
35
+ ],
36
+ pre_mutate: bool = False,
37
+ statically_defined: bool = False,
38
+ inserted_by: Optional[str] = None,
39
+ ):
40
+ from .mutable_flow import MutableFlow
41
+
42
+ self._mutable_container = MutableFlow(
43
+ flow_spec,
44
+ pre_mutate=pre_mutate,
45
+ statically_defined=statically_defined,
46
+ inserted_by=inserted_by,
47
+ )
48
+ self._flow_cls = flow_spec.__class__
49
+ self._my_step = step
50
+ self._pre_mutate = pre_mutate
51
+ self._statically_defined = statically_defined
52
+ self._inserted_by = inserted_by
53
+ if self._inserted_by is None:
54
+ # This is an error because MutableSteps should only be created by
55
+ # StepMutators or FlowMutators. We need to catch it now because otherwise
56
+ # we may put stuff on the command line (with --with) that would get added
57
+ # twice and weird behavior may ensue.
58
+ raise MetaflowException(
59
+ "MutableStep should only be created by StepMutators or FlowMutators. "
60
+ "This is an internal error."
61
+ )
62
+
63
+ @property
64
+ def flow(self) -> "metaflow.user_decorator.mutable_flow.MutableFlow":
65
+ """
66
+ The flow that contains this step
67
+
68
+ Returns
69
+ -------
70
+ MutableFlow
71
+ The flow that contains this step
72
+ """
73
+ return self._mutable_container
74
+
75
+ @property
76
+ def decorator_specs(
77
+ self,
78
+ ) -> Generator[Tuple[str, str, List[Any], Dict[str, Any]], None, None]:
79
+ """
80
+ Iterate over all the decorator specifications of this step. Note that the same
81
+ type of decorator may be present multiple times and no order is guaranteed.
82
+
83
+ The returned tuple contains:
84
+ - The decorator's name (shortest possible)
85
+ - The decorator's fully qualified name (in the case of Metaflow decorators, this
86
+ will indicate which extension the decorator comes from)
87
+ - A list of positional arguments
88
+ - A dictionary of keyword arguments
89
+
90
+ You can use the resulting tuple to remove a decorator for example
91
+
92
+ Yields
93
+ ------
94
+ str, str, List[Any], Dict[str, Any]
95
+ A tuple containing the decorator name, it's fully qualified name,
96
+ a list of positional arguments, and a dictionary of keyword arguments.
97
+ """
98
+ for deco in self._my_step.decorators:
99
+ # 3.7 does not support yield foo, *bar syntax so we
100
+ # work around
101
+ r = [
102
+ deco.name,
103
+ "%s.%s"
104
+ % (
105
+ deco.__class__.__module__,
106
+ deco.__class__.__name__,
107
+ ),
108
+ ]
109
+ r.extend(deco.get_args_kwargs())
110
+ yield tuple(r)
111
+
112
+ for deco in self._my_step.wrappers:
113
+ r = [
114
+ UserStepDecoratorBase.get_decorator_name(deco.__class__),
115
+ deco.decorator_name,
116
+ ]
117
+ r.extend(deco.get_args_kwargs())
118
+ yield tuple(r)
119
+
120
+ for deco in self._my_step.config_decorators:
121
+ r = [
122
+ UserStepDecoratorBase.get_decorator_name(deco.__class__),
123
+ deco.decorator_name,
124
+ ]
125
+ r.extend(deco.get_args_kwargs())
126
+ yield tuple(r)
127
+
128
+ def add_decorator(
129
+ self,
130
+ deco_type: Union[partial, UserStepDecoratorBase, str],
131
+ deco_args: Optional[List[Any]] = None,
132
+ deco_kwargs: Optional[Dict[str, Any]] = None,
133
+ duplicates: int = IGNORE,
134
+ ) -> None:
135
+ """
136
+ Add a Metaflow step-decorator or a user step-decorator to a step.
137
+
138
+ You can either add the decorator itself or its decorator specification for it
139
+ (the same you would get back from decorator_specs). You can also mix and match
140
+ but you cannot provide arguments both through the string and the
141
+ deco_args/deco_kwargs.
142
+
143
+ As an example:
144
+ ```
145
+ from metaflow import environment
146
+ ...
147
+ my_step.add_decorator(environment, deco_kwargs={"vars": {"foo": 42})}
148
+ ```
149
+
150
+ is equivalent to:
151
+ ```
152
+ my_step.add_decorator('environment:vars={"foo": 42}')
153
+ ```
154
+
155
+ is equivalent to:
156
+ ```
157
+ my_step.add_decorator('environment', deco_kwargs={"vars":{"foo": 42}})
158
+ ```
159
+
160
+ but this is not allowed:
161
+ ```
162
+ my_step.add_decorator('environment:vars={"bar" 43}', deco_kwargs={"vars":{"foo": 42}})
163
+ ```
164
+
165
+ Note in the case where you specify a
166
+ string for the decorator, there is no need to import the decorator.
167
+
168
+ The string syntax is useful to, for example, allow decorators to be stored as
169
+ strings in a configuration file.
170
+
171
+ You can only add StepMutators in the pre_mutate stage.
172
+
173
+ In terms of precedence for decorators:
174
+ - if a decorator can be applied multiple times (like `@card`) all decorators
175
+ added are kept.
176
+ - if `duplicates` is set to `MutableStep.IGNORE`, then the decorator
177
+ being added is ignored (in other words, the existing decorator has precedence).
178
+ - if `duplicates` is set to `MutableStep.OVERRIDE`, then the *existing*
179
+ decorator is removed and this newly added one replaces it (in other
180
+ words, the newly added decorator has precedence).
181
+ - if `duplicates` is set to `MutableStep.ERROR`, then an error is raised but only
182
+ if the newly added decorator is *static* (ie: defined directly in the code).
183
+ If not, it is ignored.
184
+
185
+ Parameters
186
+ ----------
187
+ deco_type : Union[partial, UserStepDecoratorBase, str]
188
+ The decorator class to add to this step.
189
+ deco_args : List[Any], optional, default None
190
+ Positional arguments to pass to the decorator.
191
+ deco_kwargs : Dict[str, Any], optional, default None
192
+ Keyword arguments to pass to the decorator.
193
+ duplicates : int, default MutableStep.IGNORE
194
+ Instruction on how to handle duplicates. It can be one of:
195
+ - `MutableStep.IGNORE`: Ignore the decorator if it already exists.
196
+ - `MutableStep.ERROR`: Raise an error if the decorator already exists.
197
+ - `MutableStep.OVERRIDE`: Remove the existing decorator and add this one.
198
+ """
199
+ # Prevent circular import
200
+ from metaflow.decorators import (
201
+ DuplicateStepDecoratorException,
202
+ StepDecorator,
203
+ extract_step_decorator_from_decospec,
204
+ )
205
+
206
+ deco_args = deco_args or []
207
+ deco_kwargs = deco_kwargs or {}
208
+
209
+ def _add_step_decorator(step_deco):
210
+ if deco_args:
211
+ raise MetaflowException(
212
+ "Step decorators do not take additional positional arguments"
213
+ )
214
+ # Update kwargs:
215
+ step_deco.attributes.update(deco_kwargs)
216
+
217
+ # Check duplicates
218
+ def _do_add():
219
+ step_deco.statically_defined = self._statically_defined
220
+ step_deco.inserted_by = self._inserted_by
221
+ self._my_step.decorators.append(step_deco)
222
+ debug.userconf_exec(
223
+ "Mutable step adding step decorator '%s' to step '%s'"
224
+ % (deco_type, self._my_step.name)
225
+ )
226
+
227
+ existing_deco = [
228
+ d for d in self._my_step.decorators if d.name == step_deco.name
229
+ ]
230
+
231
+ if step_deco.allow_multiple or not existing_deco:
232
+ _do_add()
233
+ elif duplicates == MutableStep.IGNORE:
234
+ # If we ignore, we do not add the decorator
235
+ debug.userconf_exec(
236
+ "Mutable step ignoring step decorator '%s' on step '%s' "
237
+ "(already exists and duplicates are ignored)"
238
+ % (step_deco.name, self._my_step.name)
239
+ )
240
+ elif duplicates == MutableStep.OVERRIDE:
241
+ # If we override, we remove the existing decorator and add this one
242
+ debug.userconf_exec(
243
+ "Mutable step overriding step decorator '%s' on step '%s' "
244
+ "(removing existing decorator and adding new one)"
245
+ % (step_deco.name, self._my_step.name)
246
+ )
247
+ self._my_step.decorators = [
248
+ d for d in self._my_step.decorators if d.name != step_deco.name
249
+ ]
250
+ _do_add()
251
+ elif duplicates == MutableStep.ERROR:
252
+ # If we error, we raise an exception
253
+ if self._statically_defined:
254
+ raise DuplicateStepDecoratorException(step_deco.name, self._my_step)
255
+ else:
256
+ debug.userconf_exec(
257
+ "Mutable step ignoring step decorator '%s' on step '%s' "
258
+ "(already exists and non statically defined)"
259
+ % (step_deco.name, self._my_step.name)
260
+ )
261
+ else:
262
+ raise ValueError("Invalid duplicates value: %s" % duplicates)
263
+
264
+ if isinstance(deco_type, str):
265
+ step_deco, has_args_kwargs = extract_step_decorator_from_decospec(deco_type)
266
+ if (deco_args or deco_kwargs) and has_args_kwargs:
267
+ raise MetaflowException(
268
+ "Cannot specify additional arguments when adding a user step "
269
+ "decorator using a decospec that already has arguments"
270
+ )
271
+
272
+ if isinstance(step_deco, StepDecorator):
273
+ _add_step_decorator(step_deco)
274
+ else:
275
+ # User defined decorator.
276
+ if not self._pre_mutate and isinstance(step_deco, StepMutator):
277
+ raise MetaflowException(
278
+ "Adding step mutator '%s' from %s is only allowed in the "
279
+ "`pre_mutate` method and not the `mutate` method"
280
+ % (step_deco.decorator_name, self._inserted_by)
281
+ )
282
+
283
+ if deco_args or deco_kwargs:
284
+ # We need to recreate the object if there were args or kwargs
285
+ # since they were not in the string
286
+ step_deco = step_deco.__class__(*deco_args, **deco_kwargs)
287
+
288
+ step_deco.add_or_raise(
289
+ self._my_step,
290
+ self._statically_defined,
291
+ duplicates,
292
+ self._inserted_by,
293
+ )
294
+ return
295
+
296
+ if isinstance(deco_type, type) and issubclass(deco_type, UserStepDecoratorBase):
297
+ # We can only add step mutators in the pre mutate stage.
298
+ if not self._pre_mutate and issubclass(deco_type, StepMutator):
299
+ raise MetaflowException(
300
+ "Adding step mutator '%s' from %s is only allowed in the "
301
+ "`pre_mutate` method and not the `mutate` method"
302
+ % (step_deco.decorator_name, self._inserted_by)
303
+ )
304
+ debug.userconf_exec(
305
+ "Mutable step adding decorator %s to step %s"
306
+ % (deco_type, self._my_step.name)
307
+ )
308
+
309
+ d = deco_type(*deco_args, **deco_kwargs)
310
+ # add_or_raise properly registers the decorator
311
+ d.add_or_raise(
312
+ self._my_step, self._statically_defined, duplicates, self._inserted_by
313
+ )
314
+ return
315
+
316
+ # At this point, it should be a regular Metaflow step decorator
317
+ if (
318
+ not isinstance(deco_type, partial)
319
+ or len(deco_type.args) != 1
320
+ or not issubclass(deco_type.args[0], StepDecorator)
321
+ ):
322
+ raise TypeError(
323
+ "add_decorator takes a metaflow decorator or user StepDecorator"
324
+ )
325
+
326
+ deco_type = deco_type.args[0]
327
+ _add_step_decorator(
328
+ deco_type(
329
+ attributes=deco_kwargs,
330
+ statically_defined=self._statically_defined,
331
+ inserted_by=self._inserted_by,
332
+ )
333
+ )
334
+
335
+ def remove_decorator(
336
+ self,
337
+ deco_name: str,
338
+ deco_args: Optional[List[Any]] = None,
339
+ deco_kwargs: Optional[Dict[str, Any]] = None,
340
+ ) -> bool:
341
+ """
342
+ Remove a step-level decorator. To remove a decorator, you can pass the decorator
343
+ specification (obtained from `decorator_specs` for example).
344
+ Note that if multiple decorators share the same decorator specification
345
+ (very rare), they will all be removed.
346
+
347
+ You can only remove StepMutators in the `pre_mutate` method.
348
+
349
+ Parameters
350
+ ----------
351
+ deco_name : str
352
+ Decorator specification of the decorator to remove. If nothing else is
353
+ specified, all decorators matching that name will be removed.
354
+ deco_args : List[Any], optional, default None
355
+ Positional arguments to match the decorator specification.
356
+ deco_kwargs : Dict[str, Any], optional, default None
357
+ Keyword arguments to match the decorator specification.
358
+
359
+ Returns
360
+ -------
361
+ bool
362
+ Returns True if a decorator was removed.
363
+ """
364
+
365
+ do_all = deco_args is None and deco_kwargs is None
366
+ did_remove = False
367
+ canonical_deco_type = UserStepDecoratorBase.get_decorator_by_name(deco_name)
368
+ if issubclass(canonical_deco_type, UserStepDecoratorBase):
369
+ for attr in ["config_decorators", "wrappers"]:
370
+ new_deco_list = []
371
+ for deco in getattr(self._my_step, attr):
372
+ if deco.decorator_name == canonical_deco_type.decorator_name:
373
+ if do_all:
374
+ continue # We remove all decorators with this name
375
+ if deco.get_args_kwargs() == (
376
+ deco_args or [],
377
+ deco_kwargs or {},
378
+ ):
379
+ if not self._pre_mutate and isinstance(deco, StepMutator):
380
+ raise MetaflowException(
381
+ "Removing step mutator '%s' from %s is only allowed in the "
382
+ "`pre_mutate` method and not the `mutate` method"
383
+ % (deco.decorator_name, self._inserted_by)
384
+ )
385
+ did_remove = True
386
+ debug.userconf_exec(
387
+ "Mutable step removing user step decorator '%s' from step '%s'"
388
+ % (deco.decorator_name, self._my_step.name)
389
+ )
390
+ else:
391
+ new_deco_list.append(deco)
392
+ else:
393
+ new_deco_list.append(deco)
394
+ setattr(self._my_step, attr, new_deco_list)
395
+
396
+ if did_remove:
397
+ return True
398
+ new_deco_list = []
399
+ for deco in self._my_step.decorators:
400
+ if deco.name == deco_name:
401
+ if do_all:
402
+ continue # We remove all decorators with this name
403
+ # Check if the decorator specification matches
404
+ if deco.get_args_kwargs() == (deco_args, deco_kwargs):
405
+ did_remove = True
406
+ debug.userconf_exec(
407
+ "Mutable step removing step decorator '%s' from step '%s'"
408
+ % (deco.name, self._my_step.name)
409
+ )
410
+ else:
411
+ new_deco_list.append(deco)
412
+ else:
413
+ new_deco_list.append(deco)
414
+
415
+ self._my_step.decorators = new_deco_list
416
+
417
+ if did_remove:
418
+ return True
419
+
420
+ debug.userconf_exec(
421
+ "Mutable step did not find decorator '%s' to remove from step '%s'"
422
+ % (deco_name, self._my_step.name)
423
+ )
424
+ return False
@@ -0,0 +1,264 @@
1
+ from typing import Dict, Optional, Union, TYPE_CHECKING
2
+
3
+ from metaflow.exception import MetaflowException
4
+ from metaflow.user_configs.config_parameters import (
5
+ resolve_delayed_evaluator,
6
+ unpack_delayed_evaluator,
7
+ )
8
+
9
+ from .common import ClassPath_Trie
10
+
11
+ if TYPE_CHECKING:
12
+ import metaflow.flowspec
13
+ import metaflow.user_decorators.mutable_flow
14
+
15
+
16
+ class FlowMutatorMeta(type):
17
+ _all_registered_decorators = ClassPath_Trie()
18
+ _do_not_register = set()
19
+ _import_modules = set()
20
+
21
+ def __new__(mcs, name, bases, namespace):
22
+ cls = super().__new__(mcs, name, bases, namespace)
23
+ cls.decorator_name = getattr(
24
+ cls, "_decorator_name", f"{cls.__module__}.{cls.__name__}"
25
+ )
26
+ if not cls.__module__.startswith("metaflow.") and not cls.__module__.startswith(
27
+ "metaflow_extensions."
28
+ ):
29
+ mcs._import_modules.add(cls.__module__)
30
+
31
+ if name == "FlowMutator" or cls.decorator_name in mcs._do_not_register:
32
+ return cls
33
+
34
+ # We inject a __init_subclass__ method so we can figure out if there
35
+ # are subclasses. We want to register as decorators only the ones that do
36
+ # not have a subclass. The logic is that everything is registered and if
37
+ # a subclass shows up, we will unregister the parent class leaving only those
38
+ # classes that do not have any subclasses registered.
39
+ @classmethod
40
+ def do_unregister(cls_, **_kwargs):
41
+ for base in cls_.__bases__:
42
+ if isinstance(base, FlowMutatorMeta):
43
+ # If the base is a FlowMutatorMeta, we unregister it
44
+ # so that we don't have any decorators that are not the
45
+ # most derived one.
46
+ mcs._all_registered_decorators.remove(base.decorator_name)
47
+ # Also make sure we don't register again
48
+ mcs._do_not_register.add(base.decorator_name)
49
+
50
+ cls.__init_subclass__ = do_unregister
51
+ mcs._all_registered_decorators.insert(cls.decorator_name, cls)
52
+ return cls
53
+
54
+ @classmethod
55
+ def all_decorators(mcs) -> Dict[str, "FlowMutatorMeta"]:
56
+ mcs._check_init()
57
+ return mcs._all_registered_decorators.get_unique_prefixes()
58
+
59
+ def __str__(cls):
60
+ return "FlowMutator(%s)" % cls.decorator_name
61
+
62
+ @classmethod
63
+ def get_decorator_by_name(
64
+ mcs, decorator_name: str
65
+ ) -> Optional[Union["FlowDecoratorMeta", "metaflow.decorators.Decorator"]]:
66
+ """
67
+ Get a decorator by its name.
68
+
69
+ Parameters
70
+ ----------
71
+ decorator_name: str
72
+ The name of the decorator to retrieve.
73
+
74
+ Returns
75
+ -------
76
+ Optional[FlowDecoratorMeta]
77
+ The decorator class if found, None otherwise.
78
+ """
79
+ mcs._check_init()
80
+ return mcs._all_registered_decorators.unique_prefix_value(decorator_name)
81
+
82
+ @classmethod
83
+ def get_decorator_name(mcs, decorator_type: type) -> Optional[str]:
84
+ """
85
+ Get the minimally unique classpath name for a decorator type.
86
+
87
+ Parameters
88
+ ----------
89
+ decorator_type: type
90
+ The type of the decorator to retrieve the name for.
91
+
92
+ Returns
93
+ -------
94
+ Optional[str]
95
+ The minimally unique classpath name if found, None otherwise.
96
+ """
97
+ mcs._check_init()
98
+ return mcs._all_registered_decorators.unique_prefix_for_type(decorator_type)
99
+
100
+ @classmethod
101
+ def _check_init(mcs):
102
+ # Delay importing STEP_DECORATORS until we actually need it
103
+ if not mcs._all_registered_decorators.inited:
104
+ from metaflow.plugins import FLOW_DECORATORS
105
+
106
+ mcs._all_registered_decorators.init([(t.name, t) for t in FLOW_DECORATORS])
107
+
108
+
109
+ class FlowMutator(metaclass=FlowMutatorMeta):
110
+ """
111
+ Derive from this class to implement a flow mutator.
112
+
113
+ A flow mutator allows you to introspect a flow and its included steps. You can
114
+ then add parameters, configurations and decorators to the flow as well as modify
115
+ any of its steps.
116
+ use values available through configurations to determine how to mutate the flow.
117
+
118
+ There are two main methods provided:
119
+ - pre_mutate: called as early as possible right after configuration values are read.
120
+ - mutate: called right after all the command line is parsed but before any
121
+ Metaflow decorators are applied.
122
+
123
+ The `mutate` method does not allow you to modify the flow itself but you can still
124
+ modify the steps.
125
+ """
126
+
127
+ def __init__(self, *args, **kwargs):
128
+ from ..flowspec import FlowSpecMeta
129
+
130
+ self._flow_cls = None
131
+ # Flow mutators are always statically defined (no way of passing them
132
+ # on the command line or adding them via configs)
133
+ self.statically_defined = True
134
+ self.inserted_by = None
135
+
136
+ # The arguments are actually passed to the init function for this decorator
137
+ # and used in _graph_info
138
+ self._args = args
139
+ self._kwargs = kwargs
140
+ if args and isinstance(args[0], (FlowMutator, FlowSpecMeta)):
141
+ # This means the decorator is bare like @MyDecorator
142
+ # and the first argument is the FlowSpec or another decorator (they
143
+ # can be stacked)
144
+
145
+ # Now set the flow class we apply to
146
+ if isinstance(args[0], FlowSpecMeta):
147
+ self._set_flow_cls(args[0])
148
+ else:
149
+ self._set_flow_cls(args[0]._flow_cls)
150
+ self._args = self._args[1:] # Remove the first argument
151
+
152
+ def __mro_entries__(self, bases):
153
+ # This is called in the following case:
154
+ # @MyMutator
155
+ # class MyBaseFlowSpec(FlowSpec):
156
+ # pass
157
+ #
158
+ # class MyFlow(MyBaseFlowSpec):
159
+ # pass
160
+ #
161
+ # MyBaseFlowSpec will be an object of type MyMutator which is not
162
+ # great when inheriting from it. With this method, we ensure that the
163
+ # base class will actually be MyBaseFlowSpec
164
+ return (self._flow_cls,)
165
+
166
+ def __call__(
167
+ self, flow_spec: Optional["metaflow.flowspec.FlowSpecMeta"] = None
168
+ ) -> "metaflow.flowspec.FlowSpecMeta":
169
+ if flow_spec:
170
+ if isinstance(flow_spec, FlowMutator):
171
+ flow_spec = flow_spec._flow_cls
172
+ return self._set_flow_cls(flow_spec)
173
+ elif not self._flow_cls:
174
+ # This means that somehow the initialization did not happen properly
175
+ # so this may have been applied to a non flow
176
+ raise MetaflowException("A FlowMutator can only be applied to a FlowSpec")
177
+ # NOTA: This returns self._flow_cls() because the object in the case of
178
+ # @FlowDecorator
179
+ # class MyFlow(FlowSpec):
180
+ # pass
181
+ # the object is a FlowDecorator and when the main function calls it, we end up
182
+ # here and need to actually call the FlowSpec. This is not the case when using
183
+ # a decorator with arguments because in the line above, we will have returned a
184
+ # FlowSpec object. Previous solution was to use __get__ but this does not seem
185
+ # to work properly.
186
+ return self._flow_cls()
187
+
188
+ def _set_flow_cls(
189
+ self, flow_spec: "metaflow.flowspec.FlowSpecMeta"
190
+ ) -> "metaflow.flowspec.FlowSpecMeta":
191
+ from ..flowspec import _FlowState
192
+
193
+ flow_spec._flow_state.setdefault(_FlowState.FLOW_MUTATORS, []).append(self)
194
+ self._flow_cls = flow_spec
195
+ return flow_spec
196
+
197
+ def __str__(self):
198
+ return str(self.__class__)
199
+
200
+ def init(self, *args, **kwargs):
201
+ """
202
+ Implement this method if you wish for your FlowMutator to take in arguments.
203
+
204
+ Your flow-mutator can then look like:
205
+
206
+ @MyMutator(arg1, arg2)
207
+ class MyFlow(FlowSpec):
208
+ pass
209
+
210
+ It is an error to use your mutator with arguments but not implement this method.
211
+
212
+ """
213
+ pass
214
+
215
+ def external_init(self):
216
+ # You can use config values in the arguments to a FlowMutator
217
+ # so we resolve those as well
218
+ self._args = [resolve_delayed_evaluator(arg) for arg in self._args]
219
+ self._kwargs, _ = unpack_delayed_evaluator(self._kwargs)
220
+ self._kwargs = {
221
+ k: resolve_delayed_evaluator(v) for k, v in self._kwargs.items()
222
+ }
223
+ if self._args or self._kwargs:
224
+ if "init" not in self.__class__.__dict__:
225
+ raise MetaflowException(
226
+ "%s is used with arguments but does not implement init" % self
227
+ )
228
+ if "init" in self.__class__.__dict__:
229
+ self.init(*self._args, **self._kwargs)
230
+
231
+ def pre_mutate(
232
+ self, mutable_flow: "metaflow.user_decorators.mutable_flow.MutableFlow"
233
+ ) -> None:
234
+ """
235
+ Method called right after all configuration values are read.
236
+
237
+ Parameters
238
+ ----------
239
+ mutable_flow : metaflow.user_decorators.mutable_flow.MutableFlow
240
+ A representation of this flow
241
+ """
242
+ return None
243
+
244
+ def mutate(
245
+ self, mutable_flow: "metaflow.user_decorators.mutable_flow.MutableFlow"
246
+ ) -> None:
247
+ """
248
+ Method called right before the first Metaflow step decorator is applied. This
249
+ means that the command line, including all `--with` options has been parsed.
250
+
251
+ Given how late this function is called, there are a few restrictions on what
252
+ you can do; the following methods on MutableFlow are not allowed and calling
253
+ them will result in an error:
254
+ - add_parameter/remove_parameter
255
+ - add_decorator/remove_decorator
256
+
257
+ To call these methods, use the `pre_mutate` method instead.
258
+
259
+ Parameters
260
+ ----------
261
+ mutable_flow : metaflow.user_decorators.mutable_flow.MutableFlow
262
+ A representation of this flow
263
+ """
264
+ return None