metaflow 2.12.35__py2.py3-none-any.whl → 2.12.37__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 (46) 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 +54 -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/parallel_decorator.py +4 -1
  29. metaflow/plugins/pypi/conda_decorator.py +22 -0
  30. metaflow/plugins/pypi/pypi_decorator.py +1 -0
  31. metaflow/plugins/timeout_decorator.py +2 -2
  32. metaflow/runner/click_api.py +73 -19
  33. metaflow/runtime.py +199 -105
  34. metaflow/sidecar/sidecar_worker.py +1 -1
  35. metaflow/user_configs/__init__.py +0 -0
  36. metaflow/user_configs/config_decorators.py +563 -0
  37. metaflow/user_configs/config_options.py +495 -0
  38. metaflow/user_configs/config_parameters.py +386 -0
  39. metaflow/util.py +17 -0
  40. metaflow/version.py +1 -1
  41. {metaflow-2.12.35.dist-info → metaflow-2.12.37.dist-info}/METADATA +3 -2
  42. {metaflow-2.12.35.dist-info → metaflow-2.12.37.dist-info}/RECORD +46 -36
  43. {metaflow-2.12.35.dist-info → metaflow-2.12.37.dist-info}/LICENSE +0 -0
  44. {metaflow-2.12.35.dist-info → metaflow-2.12.37.dist-info}/WHEEL +0 -0
  45. {metaflow-2.12.35.dist-info → metaflow-2.12.37.dist-info}/entry_points.txt +0 -0
  46. {metaflow-2.12.35.dist-info → metaflow-2.12.37.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, only_non_static=True)
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 kw_only_param and parameter.kind != inspect.Parameter.KEYWORD_ONLY:
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
 
@@ -115,14 +121,36 @@ class Decorator(object):
115
121
  def __init__(self, attributes=None, statically_defined=False):
116
122
  self.attributes = self.defaults.copy()
117
123
  self.statically_defined = statically_defined
124
+ self._user_defined_attributes = set()
125
+ self._ran_init = False
118
126
 
119
127
  if attributes:
120
128
  for k, v in attributes.items():
121
- if k in self.defaults:
129
+ if k in self.defaults or k.startswith(UNPACK_KEY):
122
130
  self.attributes[k] = v
131
+ if not k.startswith(UNPACK_KEY):
132
+ self._user_defined_attributes.add(k)
123
133
  else:
124
134
  raise InvalidDecoratorAttribute(self.name, k, self.defaults)
125
135
 
136
+ def init(self):
137
+ """
138
+ Initializes the decorator. In general, any operation you would do in __init__
139
+ should be done here.
140
+ """
141
+
142
+ # In some cases (specifically when using remove_decorator), we may need to call
143
+ # init multiple times. Short-circuit re-evaluating.
144
+ if self._ran_init:
145
+ return
146
+
147
+ # Note that by design, later values override previous ones.
148
+ self.attributes = unpack_delayed_evaluator(self.attributes)
149
+ self._user_defined_attributes.update(self.attributes.keys())
150
+ self.attributes = resolve_delayed_evaluator(self.attributes)
151
+
152
+ self._ran_init = True
153
+
126
154
  @classmethod
127
155
  def _parse_decorator_spec(cls, deco_spec):
128
156
  if len(deco_spec) == 0:
@@ -203,10 +231,13 @@ class FlowDecorator(Decorator):
203
231
 
204
232
  # compare this to parameters.add_custom_parameters
205
233
  def add_decorator_options(cmd):
206
- seen = {}
207
234
  flow_cls = getattr(current_flow, "flow_cls", None)
208
235
  if flow_cls is None:
209
236
  return cmd
237
+
238
+ seen = {}
239
+ existing_params = set(p.name.lower() for p in cmd.params)
240
+ # Add decorator options
210
241
  for deco in flow_decorators(flow_cls):
211
242
  for option, kwargs in deco.options.items():
212
243
  if option in seen:
@@ -217,7 +248,13 @@ def add_decorator_options(cmd):
217
248
  % (deco.name, option, seen[option])
218
249
  )
219
250
  raise MetaflowInternalError(msg)
251
+ elif deco.name.lower() in existing_params:
252
+ raise MetaflowInternalError(
253
+ "Flow decorator '%s' uses an option '%s' which is a reserved "
254
+ "keyword. Please use a different option name." % (deco.name, option)
255
+ )
220
256
  else:
257
+ kwargs["envvar"] = "METAFLOW_FLOW_%s" % option.upper()
221
258
  seen[option] = deco.name
222
259
  cmd.params.insert(0, click.Option(("--" + option,), **kwargs))
223
260
  return cmd
@@ -425,10 +462,13 @@ def _base_step_decorator(decotype, *args, **kwargs):
425
462
  Decorator prototype for all step decorators. This function gets specialized
426
463
  and imported for all decorators types by _import_plugin_decorators().
427
464
  """
465
+
428
466
  if args:
429
467
  # No keyword arguments specified for the decorator, e.g. @foobar.
430
468
  # The first argument is the function to be decorated.
431
469
  func = args[0]
470
+ if isinstance(func, CustomStepDecorator):
471
+ func = func._my_step
432
472
  if not hasattr(func, "is_step"):
433
473
  raise BadStepDecoratorException(decotype.name, func)
434
474
 
@@ -510,6 +550,17 @@ def _attach_decorators_to_step(step, decospecs):
510
550
  step.decorators.append(deco)
511
551
 
512
552
 
553
+ def _init(flow, only_non_static=False):
554
+ for decorators in flow._flow_decorators.values():
555
+ for deco in decorators:
556
+ if not only_non_static or not deco.statically_defined:
557
+ deco.init()
558
+ for flowstep in flow:
559
+ for deco in flowstep.decorators:
560
+ if not only_non_static or not deco.statically_defined:
561
+ deco.init()
562
+
563
+
513
564
  def _init_flow_decorators(
514
565
  flow, graph, environment, flow_datastore, metadata, logger, echo, deco_options
515
566
  ):
@@ -620,6 +671,7 @@ def step(
620
671
  """
621
672
  f.is_step = True
622
673
  f.decorators = []
674
+ f.config_decorators = []
623
675
  try:
624
676
  # python 3
625
677
  f.name = f.__name__
@@ -93,7 +93,32 @@ def merge_lists(base, overrides, attr):
93
93
  base[:] = l[:]
94
94
 
95
95
 
96
- def resolve_plugins(category):
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
- path, cls_name = class_path.rsplit(".", 1)
136
- try:
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
- to_return[name] = cls
158
- _ext_debug(
159
- " Added %s plugin '%s' from '%s'" % (category, name, class_path)
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: