metaflow 2.15.14__py2.py3-none-any.whl → 2.15.15__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 +2 -2
- metaflow/_vendor/click/core.py +4 -3
- metaflow/cmd/develop/stubs.py +9 -27
- metaflow/datastore/task_datastore.py +3 -3
- metaflow/decorators.py +3 -3
- metaflow/extension_support/__init__.py +25 -42
- metaflow/parameters.py +2 -2
- metaflow/plugins/argo/argo_workflows_cli.py +4 -4
- metaflow/plugins/cards/card_modules/chevron/renderer.py +1 -1
- metaflow/plugins/cards/card_modules/test_cards.py +6 -6
- metaflow/plugins/cards/component_serializer.py +1 -8
- metaflow/plugins/package_cli.py +12 -2
- metaflow/plugins/pypi/bootstrap.py +2 -2
- metaflow/plugins/uv/bootstrap.py +18 -1
- metaflow/plugins/uv/uv_environment.py +1 -1
- metaflow/runner/click_api.py +16 -9
- metaflow/runner/deployer_impl.py +17 -5
- metaflow/runner/metaflow_runner.py +40 -13
- metaflow/runner/subprocess_manager.py +1 -1
- metaflow/runner/utils.py +8 -0
- metaflow/user_configs/config_options.py +6 -6
- metaflow/user_configs/config_parameters.py +211 -45
- metaflow/util.py +2 -5
- metaflow/vendor.py +0 -1
- metaflow/version.py +1 -1
- {metaflow-2.15.14.dist-info → metaflow-2.15.15.dist-info}/METADATA +2 -2
- {metaflow-2.15.14.dist-info → metaflow-2.15.15.dist-info}/RECORD +34 -38
- {metaflow-2.15.14.dist-info → metaflow-2.15.15.dist-info}/WHEEL +1 -1
- metaflow/_vendor/v3_5/__init__.py +0 -1
- metaflow/_vendor/v3_5/importlib_metadata/__init__.py +0 -644
- metaflow/_vendor/v3_5/importlib_metadata/_compat.py +0 -152
- metaflow/_vendor/v3_5/zipp.py +0 -329
- {metaflow-2.15.14.data → metaflow-2.15.15.data}/data/share/metaflow/devtools/Makefile +0 -0
- {metaflow-2.15.14.data → metaflow-2.15.15.data}/data/share/metaflow/devtools/Tiltfile +0 -0
- {metaflow-2.15.14.data → metaflow-2.15.15.data}/data/share/metaflow/devtools/pick_services.sh +0 -0
- {metaflow-2.15.14.dist-info → metaflow-2.15.15.dist-info}/entry_points.txt +0 -0
- {metaflow-2.15.14.dist-info → metaflow-2.15.15.dist-info}/licenses/LICENSE +0 -0
- {metaflow-2.15.14.dist-info → metaflow-2.15.15.dist-info}/top_level.txt +0 -0
@@ -7,12 +7,15 @@ from typing import Dict, Iterator, Optional, Tuple
|
|
7
7
|
|
8
8
|
from metaflow import Run
|
9
9
|
|
10
|
+
from metaflow.metaflow_config import CLICK_API_PROCESS_CONFIG
|
11
|
+
|
10
12
|
from metaflow.plugins import get_runner_cli
|
11
13
|
|
12
14
|
from .utils import (
|
13
15
|
temporary_fifo,
|
14
16
|
handle_timeout,
|
15
17
|
async_handle_timeout,
|
18
|
+
with_dir,
|
16
19
|
)
|
17
20
|
from .subprocess_manager import CommandManager, SubprocessManager
|
18
21
|
|
@@ -299,7 +302,7 @@ class Runner(metaclass=RunnerMeta):
|
|
299
302
|
if profile:
|
300
303
|
self.env_vars["METAFLOW_PROFILE"] = profile
|
301
304
|
|
302
|
-
self.cwd = cwd
|
305
|
+
self.cwd = cwd or os.getcwd()
|
303
306
|
self.file_read_timeout = file_read_timeout
|
304
307
|
self.spm = SubprocessManager()
|
305
308
|
self.top_level_kwargs = kwargs
|
@@ -359,9 +362,15 @@ class Runner(metaclass=RunnerMeta):
|
|
359
362
|
ExecutingRun containing the results of the run.
|
360
363
|
"""
|
361
364
|
with temporary_fifo() as (attribute_file_path, attribute_file_fd):
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
+
if CLICK_API_PROCESS_CONFIG:
|
366
|
+
with with_dir(self.cwd):
|
367
|
+
command = self.api(**self.top_level_kwargs).run(
|
368
|
+
runner_attribute_file=attribute_file_path, **kwargs
|
369
|
+
)
|
370
|
+
else:
|
371
|
+
command = self.api(**self.top_level_kwargs).run(
|
372
|
+
runner_attribute_file=attribute_file_path, **kwargs
|
373
|
+
)
|
365
374
|
|
366
375
|
pid = self.spm.run_command(
|
367
376
|
[sys.executable, *command],
|
@@ -390,9 +399,15 @@ class Runner(metaclass=RunnerMeta):
|
|
390
399
|
ExecutingRun containing the results of the resumed run.
|
391
400
|
"""
|
392
401
|
with temporary_fifo() as (attribute_file_path, attribute_file_fd):
|
393
|
-
|
394
|
-
|
395
|
-
|
402
|
+
if CLICK_API_PROCESS_CONFIG:
|
403
|
+
with with_dir(self.cwd):
|
404
|
+
command = self.api(**self.top_level_kwargs).resume(
|
405
|
+
runner_attribute_file=attribute_file_path, **kwargs
|
406
|
+
)
|
407
|
+
else:
|
408
|
+
command = self.api(**self.top_level_kwargs).resume(
|
409
|
+
runner_attribute_file=attribute_file_path, **kwargs
|
410
|
+
)
|
396
411
|
|
397
412
|
pid = self.spm.run_command(
|
398
413
|
[sys.executable, *command],
|
@@ -423,9 +438,15 @@ class Runner(metaclass=RunnerMeta):
|
|
423
438
|
ExecutingRun representing the run that was started.
|
424
439
|
"""
|
425
440
|
with temporary_fifo() as (attribute_file_path, attribute_file_fd):
|
426
|
-
|
427
|
-
|
428
|
-
|
441
|
+
if CLICK_API_PROCESS_CONFIG:
|
442
|
+
with with_dir(self.cwd):
|
443
|
+
command = self.api(**self.top_level_kwargs).run(
|
444
|
+
runner_attribute_file=attribute_file_path, **kwargs
|
445
|
+
)
|
446
|
+
else:
|
447
|
+
command = self.api(**self.top_level_kwargs).run(
|
448
|
+
runner_attribute_file=attribute_file_path, **kwargs
|
449
|
+
)
|
429
450
|
|
430
451
|
pid = await self.spm.async_run_command(
|
431
452
|
[sys.executable, *command],
|
@@ -455,9 +476,15 @@ class Runner(metaclass=RunnerMeta):
|
|
455
476
|
ExecutingRun representing the resumed run that was started.
|
456
477
|
"""
|
457
478
|
with temporary_fifo() as (attribute_file_path, attribute_file_fd):
|
458
|
-
|
459
|
-
|
460
|
-
|
479
|
+
if CLICK_API_PROCESS_CONFIG:
|
480
|
+
with with_dir(self.cwd):
|
481
|
+
command = self.api(**self.top_level_kwargs).resume(
|
482
|
+
runner_attribute_file=attribute_file_path, **kwargs
|
483
|
+
)
|
484
|
+
else:
|
485
|
+
command = self.api(**self.top_level_kwargs).resume(
|
486
|
+
runner_attribute_file=attribute_file_path, **kwargs
|
487
|
+
)
|
461
488
|
|
462
489
|
pid = await self.spm.async_run_command(
|
463
490
|
[sys.executable, *command],
|
@@ -237,7 +237,7 @@ class CommandManager(object):
|
|
237
237
|
self.command = command
|
238
238
|
|
239
239
|
self.env = env if env is not None else os.environ.copy()
|
240
|
-
self.cwd = cwd
|
240
|
+
self.cwd = cwd or os.getcwd()
|
241
241
|
|
242
242
|
self.process = None
|
243
243
|
self.stdout_thread = None
|
metaflow/runner/utils.py
CHANGED
@@ -322,3 +322,11 @@ def get_lower_level_group(
|
|
322
322
|
raise ValueError(f"Sub-command '{sub_command}' not found in API '{api.name}'")
|
323
323
|
|
324
324
|
return sub_command_obj(**sub_command_kwargs)
|
325
|
+
|
326
|
+
|
327
|
+
@contextmanager
|
328
|
+
def with_dir(new_dir):
|
329
|
+
current_dir = os.getcwd()
|
330
|
+
os.chdir(new_dir)
|
331
|
+
yield new_dir
|
332
|
+
os.chdir(current_dir)
|
@@ -221,13 +221,13 @@ class ConfigInput:
|
|
221
221
|
if param_name == "config_value":
|
222
222
|
self._value_values = {
|
223
223
|
k.lower(): v
|
224
|
-
for k, v in param_value
|
224
|
+
for k, v in param_value.items()
|
225
225
|
if v is not None and not v.startswith(_CONVERTED_DEFAULT)
|
226
226
|
}
|
227
227
|
else:
|
228
228
|
self._path_values = {
|
229
229
|
k.lower(): v
|
230
|
-
for k, v in param_value
|
230
|
+
for k, v in param_value.items()
|
231
231
|
if v is not None and not v.startswith(_CONVERTED_DEFAULT)
|
232
232
|
}
|
233
233
|
if do_return:
|
@@ -329,12 +329,12 @@ class ConfigInput:
|
|
329
329
|
to_return[name] = None
|
330
330
|
flow_cls._flow_state[_FlowState.CONFIGS][name] = None
|
331
331
|
continue
|
332
|
-
if val.startswith(_CONVERTED_DEFAULT_NO_FILE):
|
333
|
-
no_default_file.append(name)
|
334
|
-
continue
|
335
332
|
if val.startswith(_CONVERTED_NO_FILE):
|
336
333
|
no_file.append(name)
|
337
334
|
continue
|
335
|
+
if val.startswith(_CONVERTED_DEFAULT_NO_FILE):
|
336
|
+
no_default_file.append(name)
|
337
|
+
continue
|
338
338
|
|
339
339
|
val = val[len(_CONVERT_PREFIX) :] # Remove the _CONVERT_PREFIX
|
340
340
|
if val.startswith(_DEFAULT_PREFIX): # Remove the _DEFAULT_PREFIX if needed
|
@@ -398,7 +398,7 @@ class ConfigInput:
|
|
398
398
|
return self.process_configs(
|
399
399
|
ctx.obj.flow.name,
|
400
400
|
param.name,
|
401
|
-
value,
|
401
|
+
dict(value),
|
402
402
|
ctx.params["quiet"],
|
403
403
|
ctx.params["datastore"],
|
404
404
|
click_obj=ctx.obj,
|
@@ -1,9 +1,22 @@
|
|
1
|
-
import
|
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
|
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
|
@@ -54,7 +67,7 @@ def dump_config_values(flow: "FlowSpec"):
|
|
54
67
|
return {}
|
55
68
|
|
56
69
|
|
57
|
-
class ConfigValue(collections.abc.Mapping):
|
70
|
+
class ConfigValue(collections.abc.Mapping, dict):
|
58
71
|
"""
|
59
72
|
ConfigValue is a thin wrapper around an arbitrarily nested dictionary-like
|
60
73
|
configuration object. It allows you to access elements of this nested structure
|
@@ -69,8 +82,67 @@ class ConfigValue(collections.abc.Mapping):
|
|
69
82
|
# Thin wrapper to allow configuration values to be accessed using a "." notation
|
70
83
|
# as well as a [] notation.
|
71
84
|
|
72
|
-
|
73
|
-
|
85
|
+
# We inherit from dict to allow the isinstanceof check to work easily and also
|
86
|
+
# to provide a simple json dumps functionality.
|
87
|
+
|
88
|
+
def __init__(self, data: Union["ConfigValue", Dict[str, Any]]):
|
89
|
+
self._data = {k: self._construct(v) for k, v in data.items()}
|
90
|
+
|
91
|
+
# Enable json dumps
|
92
|
+
dict.__init__(self, self._data)
|
93
|
+
|
94
|
+
@classmethod
|
95
|
+
def fromkeys(cls, iterable: Iterable, value: Any = None) -> "ConfigValue":
|
96
|
+
"""
|
97
|
+
Creates a new ConfigValue object from the given iterable and value.
|
98
|
+
|
99
|
+
Parameters
|
100
|
+
----------
|
101
|
+
iterable : Iterable
|
102
|
+
Iterable to create the ConfigValue from.
|
103
|
+
value : Any, optional
|
104
|
+
Value to set for each key in the iterable.
|
105
|
+
|
106
|
+
Returns
|
107
|
+
-------
|
108
|
+
ConfigValue
|
109
|
+
A new ConfigValue object.
|
110
|
+
"""
|
111
|
+
return cls(dict.fromkeys(iterable, value))
|
112
|
+
|
113
|
+
def to_dict(self) -> Dict[Any, Any]:
|
114
|
+
"""
|
115
|
+
Returns a dictionary representation of this configuration object.
|
116
|
+
|
117
|
+
Returns
|
118
|
+
-------
|
119
|
+
Dict[Any, Any]
|
120
|
+
Dictionary equivalent of this configuration object.
|
121
|
+
"""
|
122
|
+
return self._to_dict(self._data)
|
123
|
+
|
124
|
+
def copy(self) -> "ConfigValue":
|
125
|
+
return self.__copy__()
|
126
|
+
|
127
|
+
def clear(self) -> None:
|
128
|
+
# Prevent configuration modification
|
129
|
+
raise TypeError("ConfigValue is immutable")
|
130
|
+
|
131
|
+
def update(self, *args, **kwargs) -> None:
|
132
|
+
# Prevent configuration modification
|
133
|
+
raise TypeError("ConfigValue is immutable")
|
134
|
+
|
135
|
+
def setdefault(self, key: Any, default: Any = None) -> Any:
|
136
|
+
# Prevent configuration modification
|
137
|
+
raise TypeError("ConfigValue is immutable")
|
138
|
+
|
139
|
+
def pop(self, key: Any, default: Any = None) -> Any:
|
140
|
+
# Prevent configuration modification
|
141
|
+
raise TypeError("ConfigValue is immutable")
|
142
|
+
|
143
|
+
def popitem(self) -> Tuple[Any, Any]:
|
144
|
+
# Prevent configuration modification
|
145
|
+
raise TypeError("ConfigValue is immutable")
|
74
146
|
|
75
147
|
def __getattr__(self, key: str) -> Any:
|
76
148
|
"""
|
@@ -115,33 +187,93 @@ class ConfigValue(collections.abc.Mapping):
|
|
115
187
|
Any
|
116
188
|
Element of the configuration
|
117
189
|
"""
|
118
|
-
|
119
|
-
if isinstance(value, dict):
|
120
|
-
value = ConfigValue(value)
|
121
|
-
return value
|
190
|
+
return self._data[key]
|
122
191
|
|
123
|
-
def
|
192
|
+
def __setitem__(self, key: Any, value: Any) -> None:
|
193
|
+
# Prevent configuration modification
|
194
|
+
raise TypeError("ConfigValue is immutable")
|
195
|
+
|
196
|
+
def __delattr__(self, key) -> None:
|
197
|
+
# Prevent configuration modification
|
198
|
+
raise TypeError("ConfigValue is immutable")
|
199
|
+
|
200
|
+
def __delitem__(self, key: Any) -> None:
|
201
|
+
# Prevent configuration modification
|
202
|
+
raise TypeError("ConfigValue is immutable")
|
203
|
+
|
204
|
+
def __len__(self) -> int:
|
124
205
|
return len(self._data)
|
125
206
|
|
126
|
-
def __iter__(self):
|
207
|
+
def __iter__(self) -> Iterator:
|
127
208
|
return iter(self._data)
|
128
209
|
|
129
|
-
def
|
210
|
+
def __eq__(self, other: Any) -> bool:
|
211
|
+
if isinstance(other, ConfigValue):
|
212
|
+
return self._data == other._data
|
213
|
+
if isinstance(other, dict):
|
214
|
+
return self._data == other
|
215
|
+
return False
|
216
|
+
|
217
|
+
def __ne__(self, other: Any) -> bool:
|
218
|
+
return not self.__eq__(other)
|
219
|
+
|
220
|
+
def __copy__(self) -> "ConfigValue":
|
221
|
+
cls = self.__class__
|
222
|
+
result = cls.__new__(cls)
|
223
|
+
result.__dict__.update({k: copy.copy(v) for k, v in self.__dict__.items()})
|
224
|
+
return result
|
225
|
+
|
226
|
+
def __repr__(self) -> str:
|
130
227
|
return repr(self._data)
|
131
228
|
|
132
|
-
def __str__(self):
|
133
|
-
return
|
229
|
+
def __str__(self) -> str:
|
230
|
+
return str(self._data)
|
134
231
|
|
135
|
-
def
|
232
|
+
def __dir__(self) -> Iterable[str]:
|
233
|
+
return dir(type(self)) + [k for k in self._data.keys() if ID_PATTERN.match(k)]
|
234
|
+
|
235
|
+
def __contains__(self, key: Any) -> bool:
|
236
|
+
try:
|
237
|
+
self[key]
|
238
|
+
except KeyError:
|
239
|
+
return False
|
240
|
+
return True
|
241
|
+
|
242
|
+
def keys(self):
|
136
243
|
"""
|
137
|
-
Returns
|
244
|
+
Returns the keys of this configuration object.
|
138
245
|
|
139
246
|
Returns
|
140
247
|
-------
|
141
|
-
|
142
|
-
|
248
|
+
Any
|
249
|
+
Keys of this configuration object.
|
143
250
|
"""
|
144
|
-
return
|
251
|
+
return self._data.keys()
|
252
|
+
|
253
|
+
@classmethod
|
254
|
+
def _construct(cls, obj: Any) -> Any:
|
255
|
+
# Internal method to construct a ConfigValue so that all mappings internally
|
256
|
+
# are also converted to ConfigValue
|
257
|
+
if isinstance(obj, ConfigValue):
|
258
|
+
v = obj
|
259
|
+
elif isinstance(obj, collections.abc.Mapping):
|
260
|
+
v = ConfigValue({k: cls._construct(v) for k, v in obj.items()})
|
261
|
+
elif isinstance(obj, (list, tuple, set)):
|
262
|
+
v = type(obj)([cls._construct(x) for x in obj])
|
263
|
+
else:
|
264
|
+
v = obj
|
265
|
+
return v
|
266
|
+
|
267
|
+
@classmethod
|
268
|
+
def _to_dict(cls, obj: Any) -> Any:
|
269
|
+
# Internal method to convert all nested mappings to dicts
|
270
|
+
if isinstance(obj, collections.abc.Mapping):
|
271
|
+
v = {k: cls._to_dict(v) for k, v in obj.items()}
|
272
|
+
elif isinstance(obj, (list, tuple, set)):
|
273
|
+
v = type(obj)([cls._to_dict(x) for x in obj])
|
274
|
+
else:
|
275
|
+
v = obj
|
276
|
+
return v
|
145
277
|
|
146
278
|
|
147
279
|
class DelayEvaluator(collections.abc.Mapping):
|
@@ -157,8 +289,9 @@ class DelayEvaluator(collections.abc.Mapping):
|
|
157
289
|
of _unpacked_delayed_*
|
158
290
|
"""
|
159
291
|
|
160
|
-
def __init__(self, ex: str):
|
292
|
+
def __init__(self, ex: str, saved_globals: Optional[Dict[str, Any]] = None):
|
161
293
|
self._config_expr = ex
|
294
|
+
self._globals = saved_globals
|
162
295
|
if ID_PATTERN.match(self._config_expr):
|
163
296
|
# This is a variable only so allow things like config_expr("config").var
|
164
297
|
self._is_var_only = True
|
@@ -166,6 +299,21 @@ class DelayEvaluator(collections.abc.Mapping):
|
|
166
299
|
else:
|
167
300
|
self._is_var_only = False
|
168
301
|
self._access = None
|
302
|
+
self._cached_expr = None
|
303
|
+
|
304
|
+
def __copy__(self):
|
305
|
+
c = DelayEvaluator(self._config_expr)
|
306
|
+
c._access = self._access.copy() if self._access is not None else None
|
307
|
+
# Globals are not copied -- always kept as a reference
|
308
|
+
return c
|
309
|
+
|
310
|
+
def __deepcopy__(self, memo):
|
311
|
+
c = DelayEvaluator(self._config_expr)
|
312
|
+
c._access = (
|
313
|
+
copy.deepcopy(self._access, memo) if self._access is not None else None
|
314
|
+
)
|
315
|
+
# Globals are not copied -- always kept as a reference
|
316
|
+
return c
|
169
317
|
|
170
318
|
def __iter__(self):
|
171
319
|
yield "%s%d" % (UNPACK_KEY, id(self))
|
@@ -175,8 +323,15 @@ class DelayEvaluator(collections.abc.Mapping):
|
|
175
323
|
return self
|
176
324
|
if self._access is None:
|
177
325
|
raise KeyError(key)
|
178
|
-
|
179
|
-
|
326
|
+
|
327
|
+
# Make a copy so that we can support something like
|
328
|
+
# foo = delay_evaluator["blah"]
|
329
|
+
# bar = delay_evaluator["baz"]
|
330
|
+
# and don't end up with a access list that contains both "blah" and "baz"
|
331
|
+
c = self.__copy__()
|
332
|
+
c._access.append(key)
|
333
|
+
c._cached_expr = None
|
334
|
+
return c
|
180
335
|
|
181
336
|
def __len__(self):
|
182
337
|
return 1
|
@@ -184,8 +339,10 @@ class DelayEvaluator(collections.abc.Mapping):
|
|
184
339
|
def __getattr__(self, name):
|
185
340
|
if self._access is None:
|
186
341
|
raise AttributeError(name)
|
187
|
-
self.
|
188
|
-
|
342
|
+
c = self.__copy__()
|
343
|
+
c._access.append(name)
|
344
|
+
c._cached_expr = None
|
345
|
+
return c
|
189
346
|
|
190
347
|
def __call__(self, ctx=None, deploy_time=False):
|
191
348
|
from ..flowspec import _FlowState # Prevent circular import
|
@@ -199,7 +356,9 @@ class DelayEvaluator(collections.abc.Mapping):
|
|
199
356
|
"Config object can only be used directly in the FlowSpec defining them "
|
200
357
|
"(or their flow decorators)."
|
201
358
|
)
|
202
|
-
if self.
|
359
|
+
if self._cached_expr is not None:
|
360
|
+
to_eval_expr = self._cached_expr
|
361
|
+
elif self._access is not None:
|
203
362
|
# Build the final expression by adding all the fields in access as . fields
|
204
363
|
access_list = [self._config_expr]
|
205
364
|
for a in self._access:
|
@@ -212,27 +371,24 @@ class DelayEvaluator(collections.abc.Mapping):
|
|
212
371
|
raise MetaflowException(
|
213
372
|
"Field '%s' of type '%s' is not supported" % (str(a), type(a))
|
214
373
|
)
|
215
|
-
self.
|
374
|
+
to_eval_expr = self._cached_expr = ".".join(access_list)
|
375
|
+
else:
|
376
|
+
to_eval_expr = self._cached_expr = self._config_expr
|
216
377
|
# Evaluate the expression setting the config values as local variables
|
217
378
|
try:
|
218
379
|
return eval(
|
219
|
-
|
220
|
-
globals(),
|
380
|
+
to_eval_expr,
|
381
|
+
self._globals or globals(),
|
221
382
|
{
|
222
383
|
k: ConfigValue(v)
|
223
384
|
for k, v in flow_cls._flow_state.get(_FlowState.CONFIGS, {}).items()
|
224
385
|
},
|
225
386
|
)
|
226
387
|
except NameError as e:
|
227
|
-
|
228
|
-
|
229
|
-
|
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
|
388
|
+
raise MetaflowException(
|
389
|
+
"Config expression '%s' could not be evaluated: %s"
|
390
|
+
% (to_eval_expr, str(e))
|
391
|
+
) from e
|
236
392
|
|
237
393
|
|
238
394
|
def config_expr(expr: str) -> DelayEvaluator:
|
@@ -262,7 +418,10 @@ def config_expr(expr: str) -> DelayEvaluator:
|
|
262
418
|
expr : str
|
263
419
|
Expression using the config values.
|
264
420
|
"""
|
265
|
-
|
421
|
+
# Get globals where the expression is defined so that the user can use
|
422
|
+
# something like `config_expr("my_func()")` in the expression.
|
423
|
+
parent_globals = inspect.currentframe().f_back.f_globals
|
424
|
+
return DelayEvaluator(expr, saved_globals=parent_globals)
|
266
425
|
|
267
426
|
|
268
427
|
class Config(Parameter, collections.abc.Mapping):
|
@@ -385,23 +544,30 @@ class Config(Parameter, collections.abc.Mapping):
|
|
385
544
|
return DelayEvaluator(self.name.lower())[key]
|
386
545
|
|
387
546
|
|
388
|
-
def resolve_delayed_evaluator(
|
547
|
+
def resolve_delayed_evaluator(
|
548
|
+
v: Any, ignore_errors: bool = False, to_dict: bool = False
|
549
|
+
) -> Any:
|
389
550
|
# NOTE: We don't ignore errors in downstream calls because we want to have either
|
390
551
|
# all or nothing for the top-level call by the user.
|
391
552
|
try:
|
392
553
|
if isinstance(v, DelayEvaluator):
|
393
|
-
|
554
|
+
to_return = v()
|
555
|
+
if to_dict and isinstance(to_return, ConfigValue):
|
556
|
+
to_return = to_return.to_dict()
|
557
|
+
return to_return
|
394
558
|
if isinstance(v, dict):
|
395
559
|
return {
|
396
|
-
resolve_delayed_evaluator(
|
560
|
+
resolve_delayed_evaluator(
|
561
|
+
k, to_dict=to_dict
|
562
|
+
): resolve_delayed_evaluator(v, to_dict=to_dict)
|
397
563
|
for k, v in v.items()
|
398
564
|
}
|
399
565
|
if isinstance(v, list):
|
400
|
-
return [resolve_delayed_evaluator(x) for x in v]
|
566
|
+
return [resolve_delayed_evaluator(x, to_dict=to_dict) for x in v]
|
401
567
|
if isinstance(v, tuple):
|
402
|
-
return tuple(resolve_delayed_evaluator(x) for x in v)
|
568
|
+
return tuple(resolve_delayed_evaluator(x, to_dict=to_dict) for x in v)
|
403
569
|
if isinstance(v, set):
|
404
|
-
return {resolve_delayed_evaluator(x) for x in v}
|
570
|
+
return {resolve_delayed_evaluator(x, to_dict=to_dict) for x in v}
|
405
571
|
return v
|
406
572
|
except Exception as e:
|
407
573
|
if ignore_errors:
|
@@ -426,7 +592,7 @@ def unpack_delayed_evaluator(
|
|
426
592
|
else:
|
427
593
|
# k.startswith(UNPACK_KEY)
|
428
594
|
try:
|
429
|
-
new_vals = resolve_delayed_evaluator(v)
|
595
|
+
new_vals = resolve_delayed_evaluator(v, to_dict=True)
|
430
596
|
new_keys.extend(new_vals.keys())
|
431
597
|
result.update(new_vals)
|
432
598
|
except Exception as e:
|
metaflow/util.py
CHANGED
@@ -418,7 +418,7 @@ def to_pascalcase(obj):
|
|
418
418
|
if isinstance(obj, dict):
|
419
419
|
res = obj.__class__()
|
420
420
|
for k in obj:
|
421
|
-
res[re.sub("([a-zA-Z])", lambda x: x.groups()[0].upper(), k, 1)] = (
|
421
|
+
res[re.sub("([a-zA-Z])", lambda x: x.groups()[0].upper(), k, count=1)] = (
|
422
422
|
to_pascalcase(obj[k])
|
423
423
|
)
|
424
424
|
elif isinstance(obj, (list, set, tuple)):
|
@@ -467,7 +467,4 @@ def to_pod(value):
|
|
467
467
|
return str(value)
|
468
468
|
|
469
469
|
|
470
|
-
|
471
|
-
from metaflow._vendor.packaging.version import parse as version_parse
|
472
|
-
else:
|
473
|
-
from distutils.version import LooseVersion as version_parse
|
470
|
+
from metaflow._vendor.packaging.version import parse as version_parse
|
metaflow/vendor.py
CHANGED
metaflow/version.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
metaflow_version = "2.15.
|
1
|
+
metaflow_version = "2.15.15"
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: metaflow
|
3
|
-
Version: 2.15.
|
3
|
+
Version: 2.15.15
|
4
4
|
Summary: Metaflow: More AI and ML, Less Engineering
|
5
5
|
Author: Metaflow Developers
|
6
6
|
Author-email: help@metaflow.org
|
@@ -26,7 +26,7 @@ License-File: LICENSE
|
|
26
26
|
Requires-Dist: requests
|
27
27
|
Requires-Dist: boto3
|
28
28
|
Provides-Extra: stubs
|
29
|
-
Requires-Dist: metaflow-stubs==2.15.
|
29
|
+
Requires-Dist: metaflow-stubs==2.15.15; extra == "stubs"
|
30
30
|
Dynamic: author
|
31
31
|
Dynamic: author-email
|
32
32
|
Dynamic: classifier
|