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.
- metaflow/__init__.py +3 -0
- metaflow/cli.py +84 -697
- metaflow/cli_args.py +17 -0
- metaflow/cli_components/__init__.py +0 -0
- metaflow/cli_components/dump_cmd.py +96 -0
- metaflow/cli_components/init_cmd.py +51 -0
- metaflow/cli_components/run_cmds.py +358 -0
- metaflow/cli_components/step_cmd.py +189 -0
- metaflow/cli_components/utils.py +140 -0
- metaflow/cmd/develop/stub_generator.py +9 -2
- metaflow/decorators.py +63 -2
- metaflow/extension_support/plugins.py +41 -27
- metaflow/flowspec.py +156 -16
- metaflow/includefile.py +50 -22
- metaflow/metaflow_config.py +1 -1
- metaflow/package.py +17 -3
- metaflow/parameters.py +80 -23
- metaflow/plugins/__init__.py +4 -0
- metaflow/plugins/airflow/airflow_cli.py +1 -0
- metaflow/plugins/argo/argo_workflows.py +41 -1
- metaflow/plugins/argo/argo_workflows_cli.py +1 -0
- metaflow/plugins/aws/batch/batch_decorator.py +2 -2
- metaflow/plugins/aws/step_functions/step_functions.py +32 -0
- metaflow/plugins/aws/step_functions/step_functions_cli.py +1 -0
- metaflow/plugins/datatools/s3/s3op.py +3 -3
- metaflow/plugins/kubernetes/kubernetes_cli.py +1 -1
- metaflow/plugins/kubernetes/kubernetes_decorator.py +2 -2
- metaflow/plugins/pypi/conda_decorator.py +22 -0
- metaflow/plugins/pypi/pypi_decorator.py +1 -0
- metaflow/plugins/timeout_decorator.py +2 -2
- metaflow/runner/click_api.py +73 -19
- metaflow/runtime.py +111 -73
- metaflow/sidecar/sidecar_worker.py +1 -1
- metaflow/user_configs/__init__.py +0 -0
- metaflow/user_configs/config_decorators.py +563 -0
- metaflow/user_configs/config_options.py +495 -0
- metaflow/user_configs/config_parameters.py +386 -0
- metaflow/util.py +17 -0
- metaflow/version.py +1 -1
- {metaflow-2.12.36.dist-info → metaflow-2.12.38.dist-info}/METADATA +3 -2
- {metaflow-2.12.36.dist-info → metaflow-2.12.38.dist-info}/RECORD +45 -35
- {metaflow-2.12.36.dist-info → metaflow-2.12.38.dist-info}/LICENSE +0 -0
- {metaflow-2.12.36.dist-info → metaflow-2.12.38.dist-info}/WHEEL +0 -0
- {metaflow-2.12.36.dist-info → metaflow-2.12.38.dist-info}/entry_points.txt +0 -0
- {metaflow-2.12.36.dist-info → metaflow-2.12.38.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,189 @@
|
|
1
|
+
from metaflow._vendor import click
|
2
|
+
|
3
|
+
from .. import decorators, namespace
|
4
|
+
from ..cli import echo_always, echo_dev_null
|
5
|
+
from ..cli_args import cli_args
|
6
|
+
from ..exception import CommandException
|
7
|
+
from ..task import MetaflowTask
|
8
|
+
from ..unbounded_foreach import UBF_CONTROL, UBF_TASK
|
9
|
+
from ..util import decompress_list
|
10
|
+
|
11
|
+
|
12
|
+
@click.command(help="Internal command to execute a single task.", hidden=True)
|
13
|
+
@click.argument("step-name")
|
14
|
+
@click.option(
|
15
|
+
"--run-id",
|
16
|
+
default=None,
|
17
|
+
required=True,
|
18
|
+
help="ID for one execution of all steps in the flow.",
|
19
|
+
)
|
20
|
+
@click.option(
|
21
|
+
"--task-id",
|
22
|
+
default=None,
|
23
|
+
required=True,
|
24
|
+
show_default=True,
|
25
|
+
help="ID for this instance of the step.",
|
26
|
+
)
|
27
|
+
@click.option(
|
28
|
+
"--input-paths",
|
29
|
+
help="A comma-separated list of pathspecs specifying inputs for this step.",
|
30
|
+
)
|
31
|
+
@click.option(
|
32
|
+
"--input-paths-filename",
|
33
|
+
type=click.Path(exists=True, readable=True, dir_okay=False, resolve_path=True),
|
34
|
+
help="A filename containing the argument typically passed to `input-paths`",
|
35
|
+
hidden=True,
|
36
|
+
)
|
37
|
+
@click.option(
|
38
|
+
"--split-index",
|
39
|
+
type=int,
|
40
|
+
default=None,
|
41
|
+
show_default=True,
|
42
|
+
help="Index of this foreach split.",
|
43
|
+
)
|
44
|
+
@click.option(
|
45
|
+
"--tag",
|
46
|
+
"opt_tag",
|
47
|
+
multiple=True,
|
48
|
+
default=None,
|
49
|
+
help="Annotate this run with the given tag. You can specify "
|
50
|
+
"this option multiple times to attach multiple tags in "
|
51
|
+
"the task.",
|
52
|
+
)
|
53
|
+
@click.option(
|
54
|
+
"--namespace",
|
55
|
+
"opt_namespace",
|
56
|
+
default=None,
|
57
|
+
help="Change namespace from the default (your username) to the specified tag.",
|
58
|
+
)
|
59
|
+
@click.option(
|
60
|
+
"--retry-count",
|
61
|
+
default=0,
|
62
|
+
help="How many times we have attempted to run this task.",
|
63
|
+
)
|
64
|
+
@click.option(
|
65
|
+
"--max-user-code-retries",
|
66
|
+
default=0,
|
67
|
+
help="How many times we should attempt running the user code.",
|
68
|
+
)
|
69
|
+
@click.option(
|
70
|
+
"--clone-only",
|
71
|
+
default=None,
|
72
|
+
help="Pathspec of the origin task for this task to clone. Do "
|
73
|
+
"not execute anything.",
|
74
|
+
)
|
75
|
+
@click.option(
|
76
|
+
"--clone-run-id",
|
77
|
+
default=None,
|
78
|
+
help="Run id of the origin flow, if this task is part of a flow being resumed.",
|
79
|
+
)
|
80
|
+
@click.option(
|
81
|
+
"--with",
|
82
|
+
"decospecs",
|
83
|
+
multiple=True,
|
84
|
+
help="Add a decorator to this task. You can specify this "
|
85
|
+
"option multiple times to attach multiple decorators "
|
86
|
+
"to this task.",
|
87
|
+
)
|
88
|
+
@click.option(
|
89
|
+
"--ubf-context",
|
90
|
+
default="none",
|
91
|
+
type=click.Choice(["none", UBF_CONTROL, UBF_TASK]),
|
92
|
+
help="Provides additional context if this task is of type unbounded foreach.",
|
93
|
+
)
|
94
|
+
@click.option(
|
95
|
+
"--num-parallel",
|
96
|
+
default=0,
|
97
|
+
type=int,
|
98
|
+
help="Number of parallel instances of a step. Ignored in local mode (see parallel decorator code).",
|
99
|
+
)
|
100
|
+
@click.pass_context
|
101
|
+
def step(
|
102
|
+
ctx,
|
103
|
+
step_name,
|
104
|
+
opt_tag=None,
|
105
|
+
run_id=None,
|
106
|
+
task_id=None,
|
107
|
+
input_paths=None,
|
108
|
+
input_paths_filename=None,
|
109
|
+
split_index=None,
|
110
|
+
opt_namespace=None,
|
111
|
+
retry_count=None,
|
112
|
+
max_user_code_retries=None,
|
113
|
+
clone_only=None,
|
114
|
+
clone_run_id=None,
|
115
|
+
decospecs=None,
|
116
|
+
ubf_context="none",
|
117
|
+
num_parallel=None,
|
118
|
+
):
|
119
|
+
|
120
|
+
if ctx.obj.is_quiet:
|
121
|
+
echo = echo_dev_null
|
122
|
+
else:
|
123
|
+
echo = echo_always
|
124
|
+
|
125
|
+
if ubf_context == "none":
|
126
|
+
ubf_context = None
|
127
|
+
if opt_namespace is not None:
|
128
|
+
namespace(opt_namespace or None)
|
129
|
+
|
130
|
+
func = None
|
131
|
+
try:
|
132
|
+
func = getattr(ctx.obj.flow, step_name)
|
133
|
+
except:
|
134
|
+
raise CommandException("Step *%s* doesn't exist." % step_name)
|
135
|
+
if not func.is_step:
|
136
|
+
raise CommandException("Function *%s* is not a step." % step_name)
|
137
|
+
echo("Executing a step, *%s*" % step_name, fg="magenta", bold=False)
|
138
|
+
|
139
|
+
if decospecs:
|
140
|
+
decorators._attach_decorators_to_step(func, decospecs)
|
141
|
+
decorators._init(ctx.obj.flow)
|
142
|
+
|
143
|
+
step_kwargs = ctx.params
|
144
|
+
# Remove argument `step_name` from `step_kwargs`.
|
145
|
+
step_kwargs.pop("step_name", None)
|
146
|
+
# Remove `opt_*` prefix from (some) option keys.
|
147
|
+
step_kwargs = dict(
|
148
|
+
[(k[4:], v) if k.startswith("opt_") else (k, v) for k, v in step_kwargs.items()]
|
149
|
+
)
|
150
|
+
cli_args._set_step_kwargs(step_kwargs)
|
151
|
+
|
152
|
+
ctx.obj.metadata.add_sticky_tags(tags=opt_tag)
|
153
|
+
if not input_paths and input_paths_filename:
|
154
|
+
with open(input_paths_filename, mode="r", encoding="utf-8") as f:
|
155
|
+
input_paths = f.read().strip(" \n\"'")
|
156
|
+
|
157
|
+
paths = decompress_list(input_paths) if input_paths else []
|
158
|
+
|
159
|
+
task = MetaflowTask(
|
160
|
+
ctx.obj.flow,
|
161
|
+
ctx.obj.flow_datastore,
|
162
|
+
ctx.obj.metadata,
|
163
|
+
ctx.obj.environment,
|
164
|
+
ctx.obj.echo,
|
165
|
+
ctx.obj.event_logger,
|
166
|
+
ctx.obj.monitor,
|
167
|
+
ubf_context,
|
168
|
+
)
|
169
|
+
if clone_only:
|
170
|
+
task.clone_only(
|
171
|
+
step_name,
|
172
|
+
run_id,
|
173
|
+
task_id,
|
174
|
+
clone_only,
|
175
|
+
retry_count,
|
176
|
+
)
|
177
|
+
else:
|
178
|
+
task.run_step(
|
179
|
+
step_name,
|
180
|
+
run_id,
|
181
|
+
task_id,
|
182
|
+
clone_run_id,
|
183
|
+
paths,
|
184
|
+
split_index,
|
185
|
+
retry_count,
|
186
|
+
max_user_code_retries,
|
187
|
+
)
|
188
|
+
|
189
|
+
echo("Success", fg="green", bold=True, indent=True)
|
@@ -0,0 +1,140 @@
|
|
1
|
+
import importlib
|
2
|
+
from metaflow._vendor import click
|
3
|
+
from metaflow.extension_support.plugins import get_plugin
|
4
|
+
|
5
|
+
|
6
|
+
class LazyPluginCommandCollection(click.CommandCollection):
|
7
|
+
# lazy_source should only point to things that are resolved as CLI plugins.
|
8
|
+
def __init__(self, *args, lazy_sources=None, **kwargs):
|
9
|
+
super().__init__(*args, **kwargs)
|
10
|
+
# lazy_sources is a list of strings in the form
|
11
|
+
# "{plugin_name}" -> "{module-name}.{command-object-name}"
|
12
|
+
self.lazy_sources = lazy_sources or {}
|
13
|
+
self._lazy_loaded = {}
|
14
|
+
|
15
|
+
def invoke(self, ctx):
|
16
|
+
# NOTE: This is copied from MultiCommand.invoke. The change is that we
|
17
|
+
# behave like chain in the sense that we evaluate the subcommand *after*
|
18
|
+
# invoking the base command but we don't chain the commands like self.chain
|
19
|
+
# would otherwise indicate.
|
20
|
+
# The goal of this is to make sure that the first command is properly executed
|
21
|
+
# *first* prior to loading the other subcommands. It's more a lazy_subcommand_load
|
22
|
+
# than a chain.
|
23
|
+
# Look for CHANGE HERE in this code to see where the changes are made.
|
24
|
+
# If click is updated, this may also need to be updated. This version is for
|
25
|
+
# click 7.1.2.
|
26
|
+
def _process_result(value):
|
27
|
+
if self.result_callback is not None:
|
28
|
+
value = ctx.invoke(self.result_callback, value, **ctx.params)
|
29
|
+
return value
|
30
|
+
|
31
|
+
if not ctx.protected_args:
|
32
|
+
# If we are invoked without command the chain flag controls
|
33
|
+
# how this happens. If we are not in chain mode, the return
|
34
|
+
# value here is the return value of the command.
|
35
|
+
# If however we are in chain mode, the return value is the
|
36
|
+
# return value of the result processor invoked with an empty
|
37
|
+
# list (which means that no subcommand actually was executed).
|
38
|
+
if self.invoke_without_command:
|
39
|
+
# CHANGE HERE: We behave like self.chain = False here
|
40
|
+
|
41
|
+
# if not self.chain:
|
42
|
+
return click.Command.invoke(self, ctx)
|
43
|
+
# with ctx:
|
44
|
+
# click.Command.invoke(self, ctx)
|
45
|
+
# return _process_result([])
|
46
|
+
|
47
|
+
ctx.fail("Missing command.")
|
48
|
+
|
49
|
+
# Fetch args back out
|
50
|
+
args = ctx.protected_args + ctx.args
|
51
|
+
ctx.args = []
|
52
|
+
ctx.protected_args = []
|
53
|
+
# CHANGE HERE: Add saved_args so we have access to it in the command to be
|
54
|
+
# able to infer what we are calling next
|
55
|
+
ctx.saved_args = args
|
56
|
+
|
57
|
+
# If we're not in chain mode, we only allow the invocation of a
|
58
|
+
# single command but we also inform the current context about the
|
59
|
+
# name of the command to invoke.
|
60
|
+
# CHANGE HERE: We change this block to do the invoke *before* the resolve_command
|
61
|
+
# Make sure the context is entered so we do not clean up
|
62
|
+
# resources until the result processor has worked.
|
63
|
+
with ctx:
|
64
|
+
ctx.invoked_subcommand = "*" if args else None
|
65
|
+
click.Command.invoke(self, ctx)
|
66
|
+
cmd_name, cmd, args = self.resolve_command(ctx, args)
|
67
|
+
sub_ctx = cmd.make_context(cmd_name, args, parent=ctx)
|
68
|
+
with sub_ctx:
|
69
|
+
return _process_result(sub_ctx.command.invoke(sub_ctx))
|
70
|
+
|
71
|
+
# CHANGE HERE: Removed all the part of chain mode.
|
72
|
+
|
73
|
+
def list_commands(self, ctx):
|
74
|
+
base = super().list_commands(ctx)
|
75
|
+
for source_name, source in self.lazy_sources.items():
|
76
|
+
subgroup = self._lazy_load(source_name, source)
|
77
|
+
base.extend(subgroup.list_commands(ctx))
|
78
|
+
return base
|
79
|
+
|
80
|
+
def get_command(self, ctx, cmd_name):
|
81
|
+
base_cmd = super().get_command(ctx, cmd_name)
|
82
|
+
if base_cmd is not None:
|
83
|
+
return base_cmd
|
84
|
+
for source_name, source in self.lazy_sources.items():
|
85
|
+
subgroup = self._lazy_load(source_name, source)
|
86
|
+
cmd = subgroup.get_command(ctx, cmd_name)
|
87
|
+
if cmd is not None:
|
88
|
+
return cmd
|
89
|
+
return None
|
90
|
+
|
91
|
+
def _lazy_load(self, source_name, source_path):
|
92
|
+
if source_name in self._lazy_loaded:
|
93
|
+
return self._lazy_loaded[source_name]
|
94
|
+
cmd_object = get_plugin("cli", source_path, source_name)
|
95
|
+
if not isinstance(cmd_object, click.Group):
|
96
|
+
raise ValueError(
|
97
|
+
f"Lazy loading of {source_name} failed by returning "
|
98
|
+
"a non-group object"
|
99
|
+
)
|
100
|
+
self._lazy_loaded[source_name] = cmd_object
|
101
|
+
return cmd_object
|
102
|
+
|
103
|
+
|
104
|
+
class LazyGroup(click.Group):
|
105
|
+
def __init__(self, *args, lazy_subcommands=None, **kwargs):
|
106
|
+
super().__init__(*args, **kwargs)
|
107
|
+
# lazy_subcommands is a list of strings in the form
|
108
|
+
# "{command} -> "{module-name}.{command-object-name}"
|
109
|
+
self.lazy_subcommands = lazy_subcommands or {}
|
110
|
+
self._lazy_loaded = {}
|
111
|
+
|
112
|
+
def list_commands(self, ctx):
|
113
|
+
base = super().list_commands(ctx)
|
114
|
+
lazy = sorted(self.lazy_subcommands.keys())
|
115
|
+
return base + lazy
|
116
|
+
|
117
|
+
def get_command(self, ctx, cmd_name):
|
118
|
+
if cmd_name in self.lazy_subcommands:
|
119
|
+
return self._lazy_load(cmd_name)
|
120
|
+
return super().get_command(ctx, cmd_name)
|
121
|
+
|
122
|
+
def _lazy_load(self, cmd_name):
|
123
|
+
if cmd_name in self._lazy_loaded:
|
124
|
+
return self._lazy_loaded[cmd_name]
|
125
|
+
|
126
|
+
import_path = self.lazy_subcommands[cmd_name]
|
127
|
+
modname, cmd = import_path.rsplit(".", 1)
|
128
|
+
# do the import
|
129
|
+
mod = importlib.import_module(modname)
|
130
|
+
# get the Command object from that module
|
131
|
+
cmd_object = getattr(mod, cmd)
|
132
|
+
# check the result to make debugging easier. note that wrapped BaseCommand
|
133
|
+
# can be functions
|
134
|
+
if not isinstance(cmd_object, click.BaseCommand):
|
135
|
+
raise ValueError(
|
136
|
+
f"Lazy loading of {import_path} failed by returning "
|
137
|
+
f"a non-command object {type(cmd_object)}"
|
138
|
+
)
|
139
|
+
self._lazy_loaded[cmd_name] = cmd_object
|
140
|
+
return cmd_object
|
@@ -1238,25 +1238,32 @@ class StubGenerator:
|
|
1238
1238
|
buff.write(indentation + deco + "\n")
|
1239
1239
|
buff.write(indentation + "def " + name + "(")
|
1240
1240
|
kw_only_param = False
|
1241
|
+
has_var_args = False
|
1241
1242
|
for i, (par_name, parameter) in enumerate(my_sign.parameters.items()):
|
1242
1243
|
annotation = self._exploit_annotation(parameter.annotation)
|
1243
|
-
|
1244
1244
|
default = exploit_default(parameter.default)
|
1245
1245
|
|
1246
|
-
if
|
1246
|
+
if (
|
1247
|
+
kw_only_param
|
1248
|
+
and not has_var_args
|
1249
|
+
and parameter.kind != inspect.Parameter.KEYWORD_ONLY
|
1250
|
+
):
|
1247
1251
|
raise RuntimeError(
|
1248
1252
|
"In function '%s': cannot have a positional parameter after a "
|
1249
1253
|
"keyword only parameter" % name
|
1250
1254
|
)
|
1255
|
+
|
1251
1256
|
if (
|
1252
1257
|
parameter.kind == inspect.Parameter.KEYWORD_ONLY
|
1253
1258
|
and not kw_only_param
|
1259
|
+
and not has_var_args
|
1254
1260
|
):
|
1255
1261
|
kw_only_param = True
|
1256
1262
|
buff.write("*, ")
|
1257
1263
|
if parameter.kind == inspect.Parameter.VAR_KEYWORD:
|
1258
1264
|
par_name = "**%s" % par_name
|
1259
1265
|
elif parameter.kind == inspect.Parameter.VAR_POSITIONAL:
|
1266
|
+
has_var_args = True
|
1260
1267
|
par_name = "*%s" % par_name
|
1261
1268
|
|
1262
1269
|
if default:
|
metaflow/decorators.py
CHANGED
@@ -12,6 +12,12 @@ from .exception import (
|
|
12
12
|
)
|
13
13
|
|
14
14
|
from .parameters import current_flow
|
15
|
+
from .user_configs.config_decorators import CustomStepDecorator
|
16
|
+
from .user_configs.config_parameters import (
|
17
|
+
UNPACK_KEY,
|
18
|
+
resolve_delayed_evaluator,
|
19
|
+
unpack_delayed_evaluator,
|
20
|
+
)
|
15
21
|
|
16
22
|
from metaflow._vendor import click
|
17
23
|
|
@@ -21,6 +27,11 @@ except NameError:
|
|
21
27
|
unicode = str
|
22
28
|
basestring = str
|
23
29
|
|
30
|
+
# Contains the decorators on which _init was called. We want to ensure it is called
|
31
|
+
# only once on each decorator and, as the _init() function below can be called in
|
32
|
+
# several places, we need to track which decorator had their init function called
|
33
|
+
_inited_decorators = set()
|
34
|
+
|
24
35
|
|
25
36
|
class BadStepDecoratorException(MetaflowException):
|
26
37
|
headline = "Syntax error"
|
@@ -115,14 +126,36 @@ class Decorator(object):
|
|
115
126
|
def __init__(self, attributes=None, statically_defined=False):
|
116
127
|
self.attributes = self.defaults.copy()
|
117
128
|
self.statically_defined = statically_defined
|
129
|
+
self._user_defined_attributes = set()
|
130
|
+
self._ran_init = False
|
118
131
|
|
119
132
|
if attributes:
|
120
133
|
for k, v in attributes.items():
|
121
|
-
if k in self.defaults:
|
134
|
+
if k in self.defaults or k.startswith(UNPACK_KEY):
|
122
135
|
self.attributes[k] = v
|
136
|
+
if not k.startswith(UNPACK_KEY):
|
137
|
+
self._user_defined_attributes.add(k)
|
123
138
|
else:
|
124
139
|
raise InvalidDecoratorAttribute(self.name, k, self.defaults)
|
125
140
|
|
141
|
+
def init(self):
|
142
|
+
"""
|
143
|
+
Initializes the decorator. In general, any operation you would do in __init__
|
144
|
+
should be done here.
|
145
|
+
"""
|
146
|
+
|
147
|
+
# In some cases (specifically when using remove_decorator), we may need to call
|
148
|
+
# init multiple times. Short-circuit re-evaluating.
|
149
|
+
if self._ran_init:
|
150
|
+
return
|
151
|
+
|
152
|
+
# Note that by design, later values override previous ones.
|
153
|
+
self.attributes = unpack_delayed_evaluator(self.attributes)
|
154
|
+
self._user_defined_attributes.update(self.attributes.keys())
|
155
|
+
self.attributes = resolve_delayed_evaluator(self.attributes)
|
156
|
+
|
157
|
+
self._ran_init = True
|
158
|
+
|
126
159
|
@classmethod
|
127
160
|
def _parse_decorator_spec(cls, deco_spec):
|
128
161
|
if len(deco_spec) == 0:
|
@@ -203,10 +236,13 @@ class FlowDecorator(Decorator):
|
|
203
236
|
|
204
237
|
# compare this to parameters.add_custom_parameters
|
205
238
|
def add_decorator_options(cmd):
|
206
|
-
seen = {}
|
207
239
|
flow_cls = getattr(current_flow, "flow_cls", None)
|
208
240
|
if flow_cls is None:
|
209
241
|
return cmd
|
242
|
+
|
243
|
+
seen = {}
|
244
|
+
existing_params = set(p.name.lower() for p in cmd.params)
|
245
|
+
# Add decorator options
|
210
246
|
for deco in flow_decorators(flow_cls):
|
211
247
|
for option, kwargs in deco.options.items():
|
212
248
|
if option in seen:
|
@@ -217,7 +253,13 @@ def add_decorator_options(cmd):
|
|
217
253
|
% (deco.name, option, seen[option])
|
218
254
|
)
|
219
255
|
raise MetaflowInternalError(msg)
|
256
|
+
elif deco.name.lower() in existing_params:
|
257
|
+
raise MetaflowInternalError(
|
258
|
+
"Flow decorator '%s' uses an option '%s' which is a reserved "
|
259
|
+
"keyword. Please use a different option name." % (deco.name, option)
|
260
|
+
)
|
220
261
|
else:
|
262
|
+
kwargs["envvar"] = "METAFLOW_FLOW_%s" % option.upper()
|
221
263
|
seen[option] = deco.name
|
222
264
|
cmd.params.insert(0, click.Option(("--" + option,), **kwargs))
|
223
265
|
return cmd
|
@@ -425,10 +467,13 @@ def _base_step_decorator(decotype, *args, **kwargs):
|
|
425
467
|
Decorator prototype for all step decorators. This function gets specialized
|
426
468
|
and imported for all decorators types by _import_plugin_decorators().
|
427
469
|
"""
|
470
|
+
|
428
471
|
if args:
|
429
472
|
# No keyword arguments specified for the decorator, e.g. @foobar.
|
430
473
|
# The first argument is the function to be decorated.
|
431
474
|
func = args[0]
|
475
|
+
if isinstance(func, CustomStepDecorator):
|
476
|
+
func = func._my_step
|
432
477
|
if not hasattr(func, "is_step"):
|
433
478
|
raise BadStepDecoratorException(decotype.name, func)
|
434
479
|
|
@@ -510,6 +555,21 @@ def _attach_decorators_to_step(step, decospecs):
|
|
510
555
|
step.decorators.append(deco)
|
511
556
|
|
512
557
|
|
558
|
+
def _init(flow, only_non_static=False):
|
559
|
+
for decorators in flow._flow_decorators.values():
|
560
|
+
for deco in decorators:
|
561
|
+
if deco in _inited_decorators:
|
562
|
+
continue
|
563
|
+
deco.init()
|
564
|
+
_inited_decorators.add(deco)
|
565
|
+
for flowstep in flow:
|
566
|
+
for deco in flowstep.decorators:
|
567
|
+
if deco in _inited_decorators:
|
568
|
+
continue
|
569
|
+
deco.init()
|
570
|
+
_inited_decorators.add(deco)
|
571
|
+
|
572
|
+
|
513
573
|
def _init_flow_decorators(
|
514
574
|
flow, graph, environment, flow_datastore, metadata, logger, echo, deco_options
|
515
575
|
):
|
@@ -620,6 +680,7 @@ def step(
|
|
620
680
|
"""
|
621
681
|
f.is_step = True
|
622
682
|
f.decorators = []
|
683
|
+
f.config_decorators = []
|
623
684
|
try:
|
624
685
|
# python 3
|
625
686
|
f.name = f.__name__
|
@@ -93,7 +93,32 @@ def merge_lists(base, overrides, attr):
|
|
93
93
|
base[:] = l[:]
|
94
94
|
|
95
95
|
|
96
|
-
def
|
96
|
+
def get_plugin(category, class_path, name):
|
97
|
+
path, cls_name = class_path.rsplit(".", 1)
|
98
|
+
try:
|
99
|
+
plugin_module = importlib.import_module(path)
|
100
|
+
except ImportError as e:
|
101
|
+
raise ValueError(
|
102
|
+
"Cannot locate %s plugin '%s' at '%s'" % (category, name, path)
|
103
|
+
) from e
|
104
|
+
cls = getattr(plugin_module, cls_name, None)
|
105
|
+
if cls is None:
|
106
|
+
raise ValueError(
|
107
|
+
"Cannot locate '%s' class for %s plugin at '%s'"
|
108
|
+
% (cls_name, category, path)
|
109
|
+
)
|
110
|
+
extracted_name = get_plugin_name(category, cls)
|
111
|
+
if extracted_name and extracted_name != name:
|
112
|
+
raise ValueError(
|
113
|
+
"Class '%s' at '%s' for %s plugin expected to be named '%s' but got '%s'"
|
114
|
+
% (cls_name, path, category, name, extracted_name)
|
115
|
+
)
|
116
|
+
globals()[cls_name] = cls
|
117
|
+
_ext_debug(" Added %s plugin '%s' from '%s'" % (category, name, class_path))
|
118
|
+
return cls
|
119
|
+
|
120
|
+
|
121
|
+
def resolve_plugins(category, path_only=False):
|
97
122
|
# Called to return a list of classes that are the available plugins for 'category'
|
98
123
|
|
99
124
|
# The ENABLED_<category> variable is set in process_plugins
|
@@ -114,7 +139,7 @@ def resolve_plugins(category):
|
|
114
139
|
|
115
140
|
available_plugins = globals()[_dict_for_category(category)]
|
116
141
|
name_extractor = _plugin_categories[category]
|
117
|
-
if not name_extractor:
|
142
|
+
if path_only or not name_extractor:
|
118
143
|
# If we have no name function, it means we just use the name in the dictionary
|
119
144
|
# and we return a dictionary. This is for sidecars mostly as they do not have
|
120
145
|
# a field that indicates their name
|
@@ -132,32 +157,14 @@ def resolve_plugins(category):
|
|
132
157
|
"Configuration requested %s plugin '%s' but no such plugin is available"
|
133
158
|
% (category, name)
|
134
159
|
)
|
135
|
-
|
136
|
-
|
137
|
-
plugin_module = importlib.import_module(path)
|
138
|
-
except ImportError:
|
139
|
-
raise ValueError(
|
140
|
-
"Cannot locate %s plugin '%s' at '%s'" % (category, name, path)
|
141
|
-
)
|
142
|
-
cls = getattr(plugin_module, cls_name, None)
|
143
|
-
if cls is None:
|
144
|
-
raise ValueError(
|
145
|
-
"Cannot locate '%s' class for %s plugin at '%s'"
|
146
|
-
% (cls_name, category, path)
|
147
|
-
)
|
148
|
-
if name_extractor and name_extractor(cls) != name:
|
149
|
-
raise ValueError(
|
150
|
-
"Class '%s' at '%s' for %s plugin expected to be named '%s' but got '%s'"
|
151
|
-
% (cls_name, path, category, name, name_extractor(cls))
|
152
|
-
)
|
153
|
-
globals()[cls_name] = cls
|
154
|
-
if name_extractor is not None:
|
155
|
-
to_return.append(cls)
|
160
|
+
if path_only:
|
161
|
+
to_return[name] = class_path
|
156
162
|
else:
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
163
|
+
if name_extractor is not None:
|
164
|
+
to_return.append(get_plugin(category, class_path, name))
|
165
|
+
else:
|
166
|
+
to_return[name] = get_plugin(category, class_path, name)
|
167
|
+
|
161
168
|
return to_return
|
162
169
|
|
163
170
|
|
@@ -193,6 +200,13 @@ _plugin_categories = {
|
|
193
200
|
}
|
194
201
|
|
195
202
|
|
203
|
+
def get_plugin_name(category, plugin):
|
204
|
+
extractor = _plugin_categories[category]
|
205
|
+
if extractor:
|
206
|
+
return extractor(plugin)
|
207
|
+
return None
|
208
|
+
|
209
|
+
|
196
210
|
def _list_for_category(category):
|
197
211
|
# Convenience function to name the variable containing List[Tuple[str, str]] where
|
198
212
|
# each tuple contains:
|