ob-metaflow 2.15.13.1__py2.py3-none-any.whl → 2.19.7.1rc0__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 (169) hide show
  1. metaflow/__init__.py +10 -3
  2. metaflow/_vendor/imghdr/__init__.py +186 -0
  3. metaflow/_vendor/yaml/__init__.py +427 -0
  4. metaflow/_vendor/yaml/composer.py +139 -0
  5. metaflow/_vendor/yaml/constructor.py +748 -0
  6. metaflow/_vendor/yaml/cyaml.py +101 -0
  7. metaflow/_vendor/yaml/dumper.py +62 -0
  8. metaflow/_vendor/yaml/emitter.py +1137 -0
  9. metaflow/_vendor/yaml/error.py +75 -0
  10. metaflow/_vendor/yaml/events.py +86 -0
  11. metaflow/_vendor/yaml/loader.py +63 -0
  12. metaflow/_vendor/yaml/nodes.py +49 -0
  13. metaflow/_vendor/yaml/parser.py +589 -0
  14. metaflow/_vendor/yaml/reader.py +185 -0
  15. metaflow/_vendor/yaml/representer.py +389 -0
  16. metaflow/_vendor/yaml/resolver.py +227 -0
  17. metaflow/_vendor/yaml/scanner.py +1435 -0
  18. metaflow/_vendor/yaml/serializer.py +111 -0
  19. metaflow/_vendor/yaml/tokens.py +104 -0
  20. metaflow/cards.py +4 -0
  21. metaflow/cli.py +125 -21
  22. metaflow/cli_components/init_cmd.py +1 -0
  23. metaflow/cli_components/run_cmds.py +204 -40
  24. metaflow/cli_components/step_cmd.py +160 -4
  25. metaflow/client/__init__.py +1 -0
  26. metaflow/client/core.py +198 -130
  27. metaflow/client/filecache.py +59 -32
  28. metaflow/cmd/code/__init__.py +2 -1
  29. metaflow/cmd/develop/stub_generator.py +49 -18
  30. metaflow/cmd/develop/stubs.py +9 -27
  31. metaflow/cmd/make_wrapper.py +30 -0
  32. metaflow/datastore/__init__.py +1 -0
  33. metaflow/datastore/content_addressed_store.py +40 -9
  34. metaflow/datastore/datastore_set.py +10 -1
  35. metaflow/datastore/flow_datastore.py +124 -4
  36. metaflow/datastore/spin_datastore.py +91 -0
  37. metaflow/datastore/task_datastore.py +92 -6
  38. metaflow/debug.py +5 -0
  39. metaflow/decorators.py +331 -82
  40. metaflow/extension_support/__init__.py +414 -356
  41. metaflow/extension_support/_empty_file.py +2 -2
  42. metaflow/flowspec.py +322 -82
  43. metaflow/graph.py +178 -15
  44. metaflow/includefile.py +25 -3
  45. metaflow/lint.py +94 -3
  46. metaflow/meta_files.py +13 -0
  47. metaflow/metadata_provider/metadata.py +13 -2
  48. metaflow/metaflow_config.py +66 -4
  49. metaflow/metaflow_environment.py +91 -25
  50. metaflow/metaflow_profile.py +18 -0
  51. metaflow/metaflow_version.py +16 -1
  52. metaflow/package/__init__.py +673 -0
  53. metaflow/packaging_sys/__init__.py +880 -0
  54. metaflow/packaging_sys/backend.py +128 -0
  55. metaflow/packaging_sys/distribution_support.py +153 -0
  56. metaflow/packaging_sys/tar_backend.py +99 -0
  57. metaflow/packaging_sys/utils.py +54 -0
  58. metaflow/packaging_sys/v1.py +527 -0
  59. metaflow/parameters.py +6 -2
  60. metaflow/plugins/__init__.py +6 -0
  61. metaflow/plugins/airflow/airflow.py +11 -1
  62. metaflow/plugins/airflow/airflow_cli.py +16 -5
  63. metaflow/plugins/argo/argo_client.py +42 -20
  64. metaflow/plugins/argo/argo_events.py +6 -6
  65. metaflow/plugins/argo/argo_workflows.py +1023 -344
  66. metaflow/plugins/argo/argo_workflows_cli.py +396 -94
  67. metaflow/plugins/argo/argo_workflows_decorator.py +9 -0
  68. metaflow/plugins/argo/argo_workflows_deployer_objects.py +75 -49
  69. metaflow/plugins/argo/capture_error.py +5 -2
  70. metaflow/plugins/argo/conditional_input_paths.py +35 -0
  71. metaflow/plugins/argo/exit_hooks.py +209 -0
  72. metaflow/plugins/argo/param_val.py +19 -0
  73. metaflow/plugins/aws/aws_client.py +6 -0
  74. metaflow/plugins/aws/aws_utils.py +33 -1
  75. metaflow/plugins/aws/batch/batch.py +72 -5
  76. metaflow/plugins/aws/batch/batch_cli.py +24 -3
  77. metaflow/plugins/aws/batch/batch_decorator.py +57 -6
  78. metaflow/plugins/aws/step_functions/step_functions.py +28 -3
  79. metaflow/plugins/aws/step_functions/step_functions_cli.py +49 -4
  80. metaflow/plugins/aws/step_functions/step_functions_deployer.py +3 -0
  81. metaflow/plugins/aws/step_functions/step_functions_deployer_objects.py +30 -0
  82. metaflow/plugins/cards/card_cli.py +20 -1
  83. metaflow/plugins/cards/card_creator.py +24 -1
  84. metaflow/plugins/cards/card_datastore.py +21 -49
  85. metaflow/plugins/cards/card_decorator.py +58 -6
  86. metaflow/plugins/cards/card_modules/basic.py +38 -9
  87. metaflow/plugins/cards/card_modules/bundle.css +1 -1
  88. metaflow/plugins/cards/card_modules/chevron/renderer.py +1 -1
  89. metaflow/plugins/cards/card_modules/components.py +592 -3
  90. metaflow/plugins/cards/card_modules/convert_to_native_type.py +34 -5
  91. metaflow/plugins/cards/card_modules/json_viewer.py +232 -0
  92. metaflow/plugins/cards/card_modules/main.css +1 -0
  93. metaflow/plugins/cards/card_modules/main.js +56 -41
  94. metaflow/plugins/cards/card_modules/test_cards.py +22 -6
  95. metaflow/plugins/cards/component_serializer.py +1 -8
  96. metaflow/plugins/cards/metadata.py +22 -0
  97. metaflow/plugins/catch_decorator.py +9 -0
  98. metaflow/plugins/datastores/local_storage.py +12 -6
  99. metaflow/plugins/datastores/spin_storage.py +12 -0
  100. metaflow/plugins/datatools/s3/s3.py +49 -17
  101. metaflow/plugins/datatools/s3/s3op.py +113 -66
  102. metaflow/plugins/env_escape/client_modules.py +102 -72
  103. metaflow/plugins/events_decorator.py +127 -121
  104. metaflow/plugins/exit_hook/__init__.py +0 -0
  105. metaflow/plugins/exit_hook/exit_hook_decorator.py +46 -0
  106. metaflow/plugins/exit_hook/exit_hook_script.py +52 -0
  107. metaflow/plugins/kubernetes/kubernetes.py +12 -1
  108. metaflow/plugins/kubernetes/kubernetes_cli.py +11 -0
  109. metaflow/plugins/kubernetes/kubernetes_decorator.py +25 -6
  110. metaflow/plugins/kubernetes/kubernetes_job.py +12 -4
  111. metaflow/plugins/kubernetes/kubernetes_jobsets.py +31 -30
  112. metaflow/plugins/metadata_providers/local.py +76 -82
  113. metaflow/plugins/metadata_providers/service.py +13 -9
  114. metaflow/plugins/metadata_providers/spin.py +16 -0
  115. metaflow/plugins/package_cli.py +36 -24
  116. metaflow/plugins/parallel_decorator.py +11 -2
  117. metaflow/plugins/parsers.py +16 -0
  118. metaflow/plugins/pypi/bootstrap.py +7 -1
  119. metaflow/plugins/pypi/conda_decorator.py +41 -82
  120. metaflow/plugins/pypi/conda_environment.py +14 -6
  121. metaflow/plugins/pypi/micromamba.py +9 -1
  122. metaflow/plugins/pypi/pip.py +41 -5
  123. metaflow/plugins/pypi/pypi_decorator.py +4 -4
  124. metaflow/plugins/pypi/utils.py +22 -0
  125. metaflow/plugins/secrets/__init__.py +3 -0
  126. metaflow/plugins/secrets/secrets_decorator.py +14 -178
  127. metaflow/plugins/secrets/secrets_func.py +49 -0
  128. metaflow/plugins/secrets/secrets_spec.py +101 -0
  129. metaflow/plugins/secrets/utils.py +74 -0
  130. metaflow/plugins/test_unbounded_foreach_decorator.py +2 -2
  131. metaflow/plugins/timeout_decorator.py +0 -1
  132. metaflow/plugins/uv/bootstrap.py +29 -1
  133. metaflow/plugins/uv/uv_environment.py +5 -3
  134. metaflow/pylint_wrapper.py +5 -1
  135. metaflow/runner/click_api.py +79 -26
  136. metaflow/runner/deployer.py +208 -6
  137. metaflow/runner/deployer_impl.py +32 -12
  138. metaflow/runner/metaflow_runner.py +266 -33
  139. metaflow/runner/subprocess_manager.py +21 -1
  140. metaflow/runner/utils.py +27 -16
  141. metaflow/runtime.py +660 -66
  142. metaflow/task.py +255 -26
  143. metaflow/user_configs/config_options.py +33 -21
  144. metaflow/user_configs/config_parameters.py +220 -58
  145. metaflow/user_decorators/__init__.py +0 -0
  146. metaflow/user_decorators/common.py +144 -0
  147. metaflow/user_decorators/mutable_flow.py +512 -0
  148. metaflow/user_decorators/mutable_step.py +424 -0
  149. metaflow/user_decorators/user_flow_decorator.py +264 -0
  150. metaflow/user_decorators/user_step_decorator.py +749 -0
  151. metaflow/util.py +197 -7
  152. metaflow/vendor.py +23 -7
  153. metaflow/version.py +1 -1
  154. {ob_metaflow-2.15.13.1.data → ob_metaflow-2.19.7.1rc0.data}/data/share/metaflow/devtools/Makefile +13 -2
  155. {ob_metaflow-2.15.13.1.data → ob_metaflow-2.19.7.1rc0.data}/data/share/metaflow/devtools/Tiltfile +107 -7
  156. {ob_metaflow-2.15.13.1.data → ob_metaflow-2.19.7.1rc0.data}/data/share/metaflow/devtools/pick_services.sh +1 -0
  157. {ob_metaflow-2.15.13.1.dist-info → ob_metaflow-2.19.7.1rc0.dist-info}/METADATA +2 -3
  158. {ob_metaflow-2.15.13.1.dist-info → ob_metaflow-2.19.7.1rc0.dist-info}/RECORD +162 -121
  159. {ob_metaflow-2.15.13.1.dist-info → ob_metaflow-2.19.7.1rc0.dist-info}/WHEEL +1 -1
  160. metaflow/_vendor/v3_5/__init__.py +0 -1
  161. metaflow/_vendor/v3_5/importlib_metadata/__init__.py +0 -644
  162. metaflow/_vendor/v3_5/importlib_metadata/_compat.py +0 -152
  163. metaflow/_vendor/v3_5/zipp.py +0 -329
  164. metaflow/info_file.py +0 -25
  165. metaflow/package.py +0 -203
  166. metaflow/user_configs/config_decorators.py +0 -568
  167. {ob_metaflow-2.15.13.1.dist-info → ob_metaflow-2.19.7.1rc0.dist-info}/entry_points.txt +0 -0
  168. {ob_metaflow-2.15.13.1.dist-info → ob_metaflow-2.19.7.1rc0.dist-info}/licenses/LICENSE +0 -0
  169. {ob_metaflow-2.15.13.1.dist-info → ob_metaflow-2.19.7.1rc0.dist-info}/top_level.txt +0 -0
@@ -1,9 +1,22 @@
1
- import collections.abc
1
+ import inspect
2
2
  import json
3
+ import collections.abc
4
+ import copy
3
5
  import os
4
6
  import re
5
7
 
6
- from typing import Any, Callable, Dict, List, Optional, Tuple, TYPE_CHECKING, Union
8
+ from typing import (
9
+ Any,
10
+ Callable,
11
+ Dict,
12
+ Iterable,
13
+ Iterator,
14
+ List,
15
+ Optional,
16
+ Tuple,
17
+ TYPE_CHECKING,
18
+ Union,
19
+ )
7
20
 
8
21
 
9
22
  from ..exception import MetaflowException
@@ -36,25 +49,21 @@ if TYPE_CHECKING:
36
49
 
37
50
  # return tracefunc_closure
38
51
 
39
- CONFIG_FILE = os.path.join(
40
- os.path.dirname(os.path.abspath(__file__)), "CONFIG_PARAMETERS"
41
- )
42
-
43
52
  ID_PATTERN = re.compile(r"^[a-zA-Z_][a-zA-Z0-9_]*$")
44
53
 
45
54
  UNPACK_KEY = "_unpacked_delayed_"
46
55
 
47
56
 
48
57
  def dump_config_values(flow: "FlowSpec"):
49
- from ..flowspec import _FlowState # Prevent circular import
58
+ from ..flowspec import FlowStateItems # Prevent circular import
50
59
 
51
- configs = flow._flow_state.get(_FlowState.CONFIGS)
60
+ configs = flow._flow_state[FlowStateItems.CONFIGS]
52
61
  if configs:
53
62
  return {"user_configs": configs}
54
63
  return {}
55
64
 
56
65
 
57
- class ConfigValue(collections.abc.Mapping):
66
+ class ConfigValue(collections.abc.Mapping, dict):
58
67
  """
59
68
  ConfigValue is a thin wrapper around an arbitrarily nested dictionary-like
60
69
  configuration object. It allows you to access elements of this nested structure
@@ -69,8 +78,67 @@ class ConfigValue(collections.abc.Mapping):
69
78
  # Thin wrapper to allow configuration values to be accessed using a "." notation
70
79
  # as well as a [] notation.
71
80
 
72
- def __init__(self, data: Dict[str, Any]):
73
- self._data = data
81
+ # We inherit from dict to allow the isinstanceof check to work easily and also
82
+ # to provide a simple json dumps functionality.
83
+
84
+ def __init__(self, data: Union["ConfigValue", Dict[str, Any]]):
85
+ self._data = {k: self._construct(v) for k, v in data.items()}
86
+
87
+ # Enable json dumps
88
+ dict.__init__(self, self._data)
89
+
90
+ @classmethod
91
+ def fromkeys(cls, iterable: Iterable, value: Any = None) -> "ConfigValue":
92
+ """
93
+ Creates a new ConfigValue object from the given iterable and value.
94
+
95
+ Parameters
96
+ ----------
97
+ iterable : Iterable
98
+ Iterable to create the ConfigValue from.
99
+ value : Any, optional
100
+ Value to set for each key in the iterable.
101
+
102
+ Returns
103
+ -------
104
+ ConfigValue
105
+ A new ConfigValue object.
106
+ """
107
+ return cls(dict.fromkeys(iterable, value))
108
+
109
+ def to_dict(self) -> Dict[Any, Any]:
110
+ """
111
+ Returns a dictionary representation of this configuration object.
112
+
113
+ Returns
114
+ -------
115
+ Dict[Any, Any]
116
+ Dictionary equivalent of this configuration object.
117
+ """
118
+ return self._to_dict(self._data)
119
+
120
+ def copy(self) -> "ConfigValue":
121
+ return self.__copy__()
122
+
123
+ def clear(self) -> None:
124
+ # Prevent configuration modification
125
+ raise TypeError("ConfigValue is immutable")
126
+
127
+ def update(self, *args, **kwargs) -> None:
128
+ # Prevent configuration modification
129
+ raise TypeError("ConfigValue is immutable")
130
+
131
+ def setdefault(self, key: Any, default: Any = None) -> Any:
132
+ # Prevent configuration modification
133
+ raise TypeError("ConfigValue is immutable")
134
+
135
+ def pop(self, key: Any, default: Any = None) -> Any:
136
+ # Prevent configuration modification
137
+ raise TypeError("ConfigValue is immutable")
138
+
139
+ def popitem(self) -> Tuple[Any, Any]:
140
+ # Prevent configuration modification
141
+ raise TypeError("ConfigValue is immutable")
74
142
 
75
143
  def __getattr__(self, key: str) -> Any:
76
144
  """
@@ -115,33 +183,96 @@ class ConfigValue(collections.abc.Mapping):
115
183
  Any
116
184
  Element of the configuration
117
185
  """
118
- value = self._data[key]
119
- if isinstance(value, dict):
120
- value = ConfigValue(value)
121
- return value
186
+ return self._data[key]
122
187
 
123
- def __len__(self):
188
+ def __setitem__(self, key: Any, value: Any) -> None:
189
+ # Prevent configuration modification
190
+ raise TypeError("ConfigValue is immutable")
191
+
192
+ def __delattr__(self, key) -> None:
193
+ # Prevent configuration modification
194
+ raise TypeError("ConfigValue is immutable")
195
+
196
+ def __delitem__(self, key: Any) -> None:
197
+ # Prevent configuration modification
198
+ raise TypeError("ConfigValue is immutable")
199
+
200
+ def __len__(self) -> int:
124
201
  return len(self._data)
125
202
 
126
- def __iter__(self):
203
+ def __iter__(self) -> Iterator:
127
204
  return iter(self._data)
128
205
 
129
- def __repr__(self):
206
+ def __eq__(self, other: Any) -> bool:
207
+ if isinstance(other, ConfigValue):
208
+ return self._data == other._data
209
+ if isinstance(other, dict):
210
+ return self._data == other
211
+ return False
212
+
213
+ def __ne__(self, other: Any) -> bool:
214
+ return not self.__eq__(other)
215
+
216
+ def __copy__(self) -> "ConfigValue":
217
+ cls = self.__class__
218
+ result = cls.__new__(cls)
219
+ result.__dict__.update({k: copy.copy(v) for k, v in self.__dict__.items()})
220
+ return result
221
+
222
+ def __repr__(self) -> str:
130
223
  return repr(self._data)
131
224
 
132
- def __str__(self):
133
- return json.dumps(self._data)
225
+ def __str__(self) -> str:
226
+ return str(self._data)
134
227
 
135
- def to_dict(self) -> Dict[Any, Any]:
228
+ def __dir__(self) -> Iterable[str]:
229
+ return dir(type(self)) + [k for k in self._data.keys() if ID_PATTERN.match(k)]
230
+
231
+ def __contains__(self, key: Any) -> bool:
232
+ try:
233
+ self[key]
234
+ except KeyError:
235
+ return False
236
+ return True
237
+
238
+ def keys(self):
136
239
  """
137
- Returns a dictionary representation of this configuration object.
240
+ Returns the keys of this configuration object.
138
241
 
139
242
  Returns
140
243
  -------
141
- Dict[Any, Any]
142
- Dictionary equivalent of this configuration object.
244
+ Any
245
+ Keys of this configuration object.
143
246
  """
144
- return dict(self._data)
247
+ return self._data.keys()
248
+
249
+ @classmethod
250
+ def _construct(cls, obj: Any) -> Any:
251
+ # Internal method to construct a ConfigValue so that all mappings internally
252
+ # are also converted to ConfigValue
253
+ if isinstance(obj, ConfigValue):
254
+ v = obj
255
+ elif isinstance(obj, collections.abc.Mapping):
256
+ v = ConfigValue({k: cls._construct(v) for k, v in obj.items()})
257
+ elif isinstance(obj, (list, tuple, set)):
258
+ v = type(obj)([cls._construct(x) for x in obj])
259
+ else:
260
+ v = obj
261
+ return v
262
+
263
+ @classmethod
264
+ def _to_dict(cls, obj: Any) -> Any:
265
+ # Internal method to convert all nested mappings to dicts
266
+ if isinstance(obj, collections.abc.Mapping):
267
+ v = {k: cls._to_dict(v) for k, v in obj.items()}
268
+ elif isinstance(obj, (list, tuple, set)):
269
+ v = type(obj)([cls._to_dict(x) for x in obj])
270
+ else:
271
+ v = obj
272
+ return v
273
+
274
+ def __reduce__(self):
275
+ return (self.__class__, (self.to_dict(),))
145
276
 
146
277
 
147
278
  class DelayEvaluator(collections.abc.Mapping):
@@ -157,8 +288,9 @@ class DelayEvaluator(collections.abc.Mapping):
157
288
  of _unpacked_delayed_*
158
289
  """
159
290
 
160
- def __init__(self, ex: str):
291
+ def __init__(self, ex: str, saved_globals: Optional[Dict[str, Any]] = None):
161
292
  self._config_expr = ex
293
+ self._globals = saved_globals
162
294
  if ID_PATTERN.match(self._config_expr):
163
295
  # This is a variable only so allow things like config_expr("config").var
164
296
  self._is_var_only = True
@@ -166,6 +298,21 @@ class DelayEvaluator(collections.abc.Mapping):
166
298
  else:
167
299
  self._is_var_only = False
168
300
  self._access = None
301
+ self._cached_expr = None
302
+
303
+ def __copy__(self):
304
+ c = DelayEvaluator(self._config_expr)
305
+ c._access = self._access.copy() if self._access is not None else None
306
+ # Globals are not copied -- always kept as a reference
307
+ return c
308
+
309
+ def __deepcopy__(self, memo):
310
+ c = DelayEvaluator(self._config_expr)
311
+ c._access = (
312
+ copy.deepcopy(self._access, memo) if self._access is not None else None
313
+ )
314
+ # Globals are not copied -- always kept as a reference
315
+ return c
169
316
 
170
317
  def __iter__(self):
171
318
  yield "%s%d" % (UNPACK_KEY, id(self))
@@ -175,8 +322,15 @@ class DelayEvaluator(collections.abc.Mapping):
175
322
  return self
176
323
  if self._access is None:
177
324
  raise KeyError(key)
178
- self._access.append(key)
179
- return self
325
+
326
+ # Make a copy so that we can support something like
327
+ # foo = delay_evaluator["blah"]
328
+ # bar = delay_evaluator["baz"]
329
+ # and don't end up with a access list that contains both "blah" and "baz"
330
+ c = self.__copy__()
331
+ c._access.append(key)
332
+ c._cached_expr = None
333
+ return c
180
334
 
181
335
  def __len__(self):
182
336
  return 1
@@ -184,11 +338,13 @@ class DelayEvaluator(collections.abc.Mapping):
184
338
  def __getattr__(self, name):
185
339
  if self._access is None:
186
340
  raise AttributeError(name)
187
- self._access.append(name)
188
- return self
341
+ c = self.__copy__()
342
+ c._access.append(name)
343
+ c._cached_expr = None
344
+ return c
189
345
 
190
346
  def __call__(self, ctx=None, deploy_time=False):
191
- from ..flowspec import _FlowState # Prevent circular import
347
+ from ..flowspec import FlowStateItems # Prevent circular import
192
348
 
193
349
  # Two additional arguments are only used by DeployTimeField which will call
194
350
  # this function with those two additional arguments. They are ignored.
@@ -199,7 +355,9 @@ class DelayEvaluator(collections.abc.Mapping):
199
355
  "Config object can only be used directly in the FlowSpec defining them "
200
356
  "(or their flow decorators)."
201
357
  )
202
- if self._access is not None:
358
+ if self._cached_expr is not None:
359
+ to_eval_expr = self._cached_expr
360
+ elif self._access is not None:
203
361
  # Build the final expression by adding all the fields in access as . fields
204
362
  access_list = [self._config_expr]
205
363
  for a in self._access:
@@ -212,27 +370,24 @@ class DelayEvaluator(collections.abc.Mapping):
212
370
  raise MetaflowException(
213
371
  "Field '%s' of type '%s' is not supported" % (str(a), type(a))
214
372
  )
215
- self._config_expr = ".".join(access_list)
373
+ to_eval_expr = self._cached_expr = ".".join(access_list)
374
+ else:
375
+ to_eval_expr = self._cached_expr = self._config_expr
216
376
  # Evaluate the expression setting the config values as local variables
217
377
  try:
218
378
  return eval(
219
- self._config_expr,
220
- globals(),
379
+ to_eval_expr,
380
+ self._globals or globals(),
221
381
  {
222
- k: ConfigValue(v)
223
- for k, v in flow_cls._flow_state.get(_FlowState.CONFIGS, {}).items()
382
+ k: ConfigValue(v) if v is not None else None
383
+ for k, v in flow_cls._flow_state[FlowStateItems.CONFIGS].items()
224
384
  },
225
385
  )
226
386
  except NameError as e:
227
- potential_config_name = self._config_expr.split(".")[0]
228
- if potential_config_name not in flow_cls._flow_state.get(
229
- _FlowState.CONFIGS, {}
230
- ):
231
- raise MetaflowException(
232
- "Config '%s' not found in the flow (maybe not required and not "
233
- "provided?)" % potential_config_name
234
- ) from e
235
- raise
387
+ raise MetaflowException(
388
+ "Config expression '%s' could not be evaluated: %s"
389
+ % (to_eval_expr, str(e))
390
+ ) from e
236
391
 
237
392
 
238
393
  def config_expr(expr: str) -> DelayEvaluator:
@@ -262,7 +417,10 @@ def config_expr(expr: str) -> DelayEvaluator:
262
417
  expr : str
263
418
  Expression using the config values.
264
419
  """
265
- return DelayEvaluator(expr)
420
+ # Get globals where the expression is defined so that the user can use
421
+ # something like `config_expr("my_func()")` in the expression.
422
+ parent_globals = inspect.currentframe().f_back.f_globals
423
+ return DelayEvaluator(expr, saved_globals=parent_globals)
266
424
 
267
425
 
268
426
  class Config(Parameter, collections.abc.Mapping):
@@ -329,7 +487,6 @@ class Config(Parameter, collections.abc.Mapping):
329
487
  parser: Optional[Union[str, Callable[[str], Dict[Any, Any]]]] = None,
330
488
  **kwargs: Dict[str, str]
331
489
  ):
332
-
333
490
  if default is not None and default_value is not None:
334
491
  raise MetaflowException(
335
492
  "For config '%s', you can only specify default or default_value, not both"
@@ -350,9 +507,7 @@ class Config(Parameter, collections.abc.Mapping):
350
507
  self._delayed_evaluator = None
351
508
 
352
509
  def load_parameter(self, v):
353
- if v is None:
354
- return None
355
- return ConfigValue(v)
510
+ return ConfigValue(v) if v is not None else None
356
511
 
357
512
  def _store_value(self, v: Any) -> None:
358
513
  self._computed_value = v
@@ -385,23 +540,30 @@ class Config(Parameter, collections.abc.Mapping):
385
540
  return DelayEvaluator(self.name.lower())[key]
386
541
 
387
542
 
388
- def resolve_delayed_evaluator(v: Any, ignore_errors: bool = False) -> Any:
543
+ def resolve_delayed_evaluator(
544
+ v: Any, ignore_errors: bool = False, to_dict: bool = False
545
+ ) -> Any:
389
546
  # NOTE: We don't ignore errors in downstream calls because we want to have either
390
547
  # all or nothing for the top-level call by the user.
391
548
  try:
392
549
  if isinstance(v, DelayEvaluator):
393
- return v()
550
+ to_return = v()
551
+ if to_dict and isinstance(to_return, ConfigValue):
552
+ to_return = to_return.to_dict()
553
+ return to_return
394
554
  if isinstance(v, dict):
395
555
  return {
396
- resolve_delayed_evaluator(k): resolve_delayed_evaluator(v)
556
+ resolve_delayed_evaluator(
557
+ k, to_dict=to_dict
558
+ ): resolve_delayed_evaluator(v, to_dict=to_dict)
397
559
  for k, v in v.items()
398
560
  }
399
561
  if isinstance(v, list):
400
- return [resolve_delayed_evaluator(x) for x in v]
562
+ return [resolve_delayed_evaluator(x, to_dict=to_dict) for x in v]
401
563
  if isinstance(v, tuple):
402
- return tuple(resolve_delayed_evaluator(x) for x in v)
564
+ return tuple(resolve_delayed_evaluator(x, to_dict=to_dict) for x in v)
403
565
  if isinstance(v, set):
404
- return {resolve_delayed_evaluator(x) for x in v}
566
+ return {resolve_delayed_evaluator(x, to_dict=to_dict) for x in v}
405
567
  return v
406
568
  except Exception as e:
407
569
  if ignore_errors:
@@ -426,7 +588,7 @@ def unpack_delayed_evaluator(
426
588
  else:
427
589
  # k.startswith(UNPACK_KEY)
428
590
  try:
429
- new_vals = resolve_delayed_evaluator(v)
591
+ new_vals = resolve_delayed_evaluator(v, to_dict=True)
430
592
  new_keys.extend(new_vals.keys())
431
593
  result.update(new_vals)
432
594
  except Exception as e:
File without changes
@@ -0,0 +1,144 @@
1
+ from typing import Dict, Optional, List, Tuple
2
+
3
+
4
+ class _TrieNode:
5
+ def __init__(
6
+ self, parent: Optional["_TrieNode"] = None, component: Optional[str] = None
7
+ ):
8
+ self.parent = parent
9
+ self.component = component
10
+ self.children = {} # type: Dict[str, "_TrieNode"]
11
+ self.total_children = 0
12
+ self.value = None
13
+ self.end_value = None
14
+
15
+ def traverse(self, value: type) -> Optional["_TrieNode"]:
16
+ if self.total_children == 0:
17
+ self.end_value = value
18
+ else:
19
+ self.end_value = None
20
+ self.total_children += 1
21
+
22
+ def remove_child(self, child_name: str) -> bool:
23
+ if child_name in self.children:
24
+ del self.children[child_name]
25
+ self.total_children -= 1
26
+ return True
27
+ return False
28
+
29
+
30
+ class ClassPath_Trie:
31
+ def __init__(self):
32
+ self.root = _TrieNode(None, None)
33
+ self.inited = False
34
+ self._value_to_node = {} # type: Dict[type, _TrieNode]
35
+
36
+ def init(self, initial_nodes: Optional[List[Tuple[str, type]]] = None):
37
+ # We need to do this so we can delay import of STEP_DECORATORS
38
+ self.inited = True
39
+ for classpath_name, value in initial_nodes or []:
40
+ self.insert(classpath_name, value)
41
+
42
+ def insert(self, classpath_name: str, value: type):
43
+ node = self.root
44
+ components = reversed(classpath_name.split("."))
45
+ for c in components:
46
+ node = node.children.setdefault(c, _TrieNode(node, c))
47
+ node.traverse(value)
48
+ node.total_children -= (
49
+ 1 # We do not count the last node as having itself as a child
50
+ )
51
+ node.value = value
52
+ self._value_to_node[value] = node
53
+
54
+ def search(self, classpath_name: str) -> Optional[type]:
55
+ node = self.root
56
+ components = reversed(classpath_name.split("."))
57
+ for c in components:
58
+ if c not in node.children:
59
+ return None
60
+ node = node.children[c]
61
+ return node.value
62
+
63
+ def remove(self, classpath_name: str):
64
+ components = list(reversed(classpath_name.split(".")))
65
+
66
+ def _remove(node: _TrieNode, components, depth):
67
+ if depth == len(components):
68
+ if node.value is not None:
69
+ del self._value_to_node[node.value]
70
+ node.value = None
71
+ return len(node.children) == 0
72
+ return False
73
+ c = components[depth]
74
+ if c not in node.children:
75
+ return False
76
+ did_delete_child = _remove(node.children[c], components, depth + 1)
77
+ if did_delete_child:
78
+ node.remove_child(c)
79
+ if node.total_children == 1:
80
+ # If we have one total child left, we have at least one
81
+ # child and that one has an end_value
82
+ for child in node.children.values():
83
+ assert (
84
+ child.end_value
85
+ ), "Node with one child must have an end_value"
86
+ node.end_value = child.end_value
87
+ return node.total_children == 0
88
+ return False
89
+
90
+ _remove(self.root, components, 0)
91
+
92
+ def unique_prefix_value(self, classpath_name: str) -> Optional[type]:
93
+ node = self.root
94
+ components = reversed(classpath_name.split("."))
95
+ for c in components:
96
+ if c not in node.children:
97
+ return None
98
+ node = node.children[c]
99
+ # If we reach here, it means the classpath_name is a prefix.
100
+ # We check if it has only one path forward (end_value will be non None)
101
+ # If value is not None, we also consider this to be a unique "prefix"
102
+ # This happens since this trie is also filled with metaflow default decorators
103
+ return node.end_value or node.value
104
+
105
+ def unique_prefix_for_type(self, value: type) -> Optional[str]:
106
+ node = self._value_to_node.get(value, None)
107
+ if node is None:
108
+ return None
109
+ components = []
110
+ while node:
111
+ if node.end_value == value:
112
+ components = []
113
+ if node.component is not None:
114
+ components.append(node.component)
115
+ node = node.parent
116
+ return ".".join(components)
117
+
118
+ def get_unique_prefixes(self) -> Dict[str, type]:
119
+ """
120
+ Get all unique prefixes in the trie.
121
+
122
+ Returns
123
+ -------
124
+ List[str]
125
+ A list of unique prefixes.
126
+ """
127
+ to_return = {}
128
+
129
+ def _collect(node, current_prefix):
130
+ if node.end_value is not None:
131
+ to_return[current_prefix] = node.end_value
132
+ # We stop there and don't look further since we found the unique prefix
133
+ return
134
+ if node.value is not None:
135
+ to_return[current_prefix] = node.value
136
+ # We continue to look for more unique prefixes
137
+ for child_name, child_node in node.children.items():
138
+ _collect(
139
+ child_node,
140
+ f"{current_prefix}.{child_name}" if current_prefix else child_name,
141
+ )
142
+
143
+ _collect(self.root, "")
144
+ return {".".join(reversed(k.split("."))): v for k, v in to_return.items()}