Skip to content

Commit 7089145

Browse files
committed
Add plain option for Config and fix dashes and capitalization
1 parent cc637df commit 7089145

File tree

7 files changed

+87
-54
lines changed

7 files changed

+87
-54
lines changed

metaflow/cli.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -444,12 +444,16 @@ def start(
444444
# We can now set the the CONFIGS value in the flow properly. This will overwrite
445445
# anything that may have been passed in by default and we will use exactly what
446446
# the original flow had. Note that these are accessed through the parameter name
447+
# We need to save the "plane-ness" flag to carry it over
448+
config_plain_flags = {
449+
k: v[1] for k, v in ctx.obj.flow._flow_state[FlowStateItems.CONFIGS].items()
450+
}
447451
ctx.obj.flow._flow_state[FlowStateItems.CONFIGS].clear()
448452
d = ctx.obj.flow._flow_state[FlowStateItems.CONFIGS]
449453
for param_name, var_name in zip(config_param_names, config_var_names):
450454
val = param_ds[var_name]
451455
debug.userconf_exec("Loaded config %s as: %s" % (param_name, val))
452-
d[param_name] = val
456+
d[param_name] = (val, config_plain_flags[param_name])
453457

454458
elif getattr(ctx.obj, "delayed_config_exception", None):
455459
# If we are not doing a resume, any exception we had parsing configs needs to
@@ -532,8 +536,8 @@ def start(
532536
ctx.obj.echo,
533537
ctx.obj.flow_datastore,
534538
{
535-
k: ConfigValue(v) if v is not None else None
536-
for k, v in ctx.obj.flow.__class__._flow_state[
539+
k: v if plain_flag else ConfigValue(v)
540+
for k, (v, plain_flag) in ctx.obj.flow.__class__._flow_state[
537541
FlowStateItems.CONFIGS
538542
].items()
539543
},

metaflow/flowspec.py

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -91,38 +91,38 @@ class _FlowState(MutableMapping):
9191
# and the ones that are inherited from parent classes.
9292
# This is NOT a general purpose class and is meant to only work with FlowSpec.
9393
# For example, it assumes that items are only list, dicts or None and assumes that
94-
# self._data has all keys properly initialized.
94+
# self._self_data has all keys properly initialized.
9595

9696
def __init__(self, *args, **kwargs):
9797
self._self_data = dict(*args, **kwargs)
98-
self._data = {}
98+
self._merged_data = {}
9999
self._inherited = {}
100100

101101
def __getitem__(self, key):
102102
# ORDER IS IMPORTANT: we use inherited first and extend by whatever is in
103103
# the flowspec
104-
if key in self._data:
105-
return self._data[key]
104+
if key in self._merged_data:
105+
return self._merged_data[key]
106106

107107
# We haven't accessed this yet so compute it for the first time
108108
self_value = self._self_data.get(key)
109109
inherited_value = self._inherited.get(key)
110110

111111
if self_value is not None:
112-
self._data[key] = self._merge_value(inherited_value, self_value)
113-
return self._data[key]
112+
self._merged_data[key] = self._merge_value(inherited_value, self_value)
113+
return self._merged_data[key]
114114
elif key in self._self_data:
115115
# Case of CACHED_PARAMETERS; a valid value is None. It is never inherited
116-
self._data[key] = None
116+
self._merged_data[key] = None
117117
return None
118118
raise KeyError(key)
119119

120120
def __setitem__(self, key, value):
121121
self._self_data[key] = value
122122

123123
def __delitem__(self, key):
124-
if key in self._data:
125-
del self._data[key]
124+
if key in self._merged_data:
125+
del self._merged_data[key]
126126
else:
127127
raise KeyError(key)
128128

@@ -136,7 +136,7 @@ def __len__(self):
136136

137137
@property
138138
def self_data(self):
139-
self._data.clear()
139+
self._merged_data.clear()
140140
return self._self_data
141141

142142
@property
@@ -477,9 +477,7 @@ def _set_constants(self, graph, kwargs, config_options):
477477
seen.add(var)
478478
if param.IS_CONFIG_PARAMETER:
479479
# Use computed value if already evaluated, else get from config_options
480-
val = param._computed_value or config_options.get(
481-
param.name.replace("-", "_").lower()
482-
)
480+
val = param._computed_value or config_options.get(param.name)
483481
else:
484482
val = kwargs[param.name.replace("-", "_").lower()]
485483
# Support for delayed evaluation of parameters.

metaflow/includefile.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -351,7 +351,7 @@ def load_parameter(self, v):
351351
# If a parser is specified, use it to parse the content
352352
if self._parser is not None:
353353
try:
354-
return ConfigInput._call_parser(self._parser, content)
354+
return ConfigInput._call_parser(self._parser, content, True)
355355
except Exception as e:
356356
raise MetaflowException(
357357
"Failed to parse content in parameter '%s' using parser: %s"

metaflow/parameters.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -347,7 +347,7 @@ def __init__(
347347
help: Optional[str] = None,
348348
required: Optional[bool] = None,
349349
show_default: Optional[bool] = None,
350-
**kwargs: Dict[str, Any]
350+
**kwargs: Dict[str, Any],
351351
):
352352
self.name = name
353353
self.kwargs = kwargs

metaflow/user_configs/config_options.py

Lines changed: 46 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import importlib
22
import json
33
import os
4-
4+
from collections.abc import Mapping
55
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
66

77
from metaflow._vendor import click
@@ -157,7 +157,7 @@ def make_key_name(name: str) -> str:
157157
# Special mark to indicate that the configuration value is not content or a file
158158
# name but a value that should be read in the config file (effectively where
159159
# the value has already been materialized).
160-
return "kv." + name.lower()
160+
return "kv." + name
161161

162162
@classmethod
163163
def set_config_file(cls, config_file: str):
@@ -225,13 +225,13 @@ def process_configs(
225225
# and is clearer
226226
if param_name == "config_value":
227227
self._value_values = {
228-
k.lower(): v
228+
k: v
229229
for k, v in param_value.items()
230230
if v is not None and not v.startswith(_CONVERTED_DEFAULT)
231231
}
232232
else:
233233
self._path_values = {
234-
k.lower(): v
234+
k: v
235235
for k, v in param_value.items()
236236
if v is not None and not v.startswith(_CONVERTED_DEFAULT)
237237
}
@@ -286,7 +286,7 @@ def process_configs(
286286
merged_configs = {}
287287
# Now look at everything (including defaults)
288288
for name, (val, is_path) in self._defaults.items():
289-
n = name.lower()
289+
n = name
290290
if n in all_values:
291291
# We have the value provided by the user -- use that.
292292
merged_configs[n] = all_values[n]
@@ -331,7 +331,10 @@ def process_configs(
331331
if val is None:
332332
missing_configs.add(name)
333333
to_return[name] = None
334-
flow_cls._flow_state.self_data[FlowStateItems.CONFIGS][name] = None
334+
flow_cls._flow_state.self_data[FlowStateItems.CONFIGS][name] = (
335+
None,
336+
True,
337+
)
335338
continue
336339
if val.startswith(_CONVERTED_NO_FILE):
337340
no_file.append(name)
@@ -340,13 +343,14 @@ def process_configs(
340343
no_default_file.append(name)
341344
continue
342345

346+
parser, is_plain = self._parsers[name]
343347
val = val[len(_CONVERT_PREFIX) :] # Remove the _CONVERT_PREFIX
344348
if val.startswith(_DEFAULT_PREFIX): # Remove the _DEFAULT_PREFIX if needed
345349
val = val[len(_DEFAULT_PREFIX) :]
346350
if val.startswith("kv."):
347351
# This means to load it from a file
348352
try:
349-
read_value = self.get_config(val[3:])
353+
read_value, read_is_plain = self.get_config(val[3:])
350354
except KeyError as e:
351355
exc = click.UsageError(
352356
"Could not find configuration '%s' in INFO file" % val
@@ -355,15 +359,23 @@ def process_configs(
355359
click_obj.delayed_config_exception = exc
356360
return None
357361
raise exc from e
358-
flow_cls._flow_state.self_data[FlowStateItems.CONFIGS][
359-
name
360-
] = read_value
362+
if read_is_plain != is_plain:
363+
raise click.UsageError(
364+
"Configuration '%s' mismatched `plain` attribute -- "
365+
"this is a bug, please report it." % val[3:]
366+
)
367+
flow_cls._flow_state.self_data[FlowStateItems.CONFIGS][name] = (
368+
read_value,
369+
True if read_value is None else is_plain,
370+
)
361371
to_return[name] = (
362-
ConfigValue(read_value) if read_value is not None else None
372+
read_value
373+
if read_value is None or is_plain
374+
else ConfigValue(read_value)
363375
)
364376
else:
365-
if self._parsers[name]:
366-
read_value = self._call_parser(self._parsers[name], val)
377+
if parser:
378+
read_value = self._call_parser(parser, val, is_plain)
367379
else:
368380
try:
369381
read_value = json.loads(val)
@@ -374,11 +386,14 @@ def process_configs(
374386
)
375387
continue
376388
# TODO: Support YAML
377-
flow_cls._flow_state.self_data[FlowStateItems.CONFIGS][
378-
name
379-
] = read_value
389+
flow_cls._flow_state.self_data[FlowStateItems.CONFIGS][name] = (
390+
read_value,
391+
True if read_value is None else is_plain,
392+
)
380393
to_return[name] = (
381-
ConfigValue(read_value) if read_value is not None else None
394+
read_value
395+
if read_value is None or is_plain
396+
else ConfigValue(read_value)
382397
)
383398

384399
reqs = missing_configs.intersection(self._req_configs)
@@ -423,7 +438,7 @@ def __repr__(self):
423438
return "ConfigInput"
424439

425440
@staticmethod
426-
def _call_parser(parser, val):
441+
def _call_parser(parser, val, is_plain):
427442
if isinstance(parser, str):
428443
if len(parser) and parser[0] == ".":
429444
parser = "metaflow" + parser
@@ -438,7 +453,13 @@ def _call_parser(parser, val):
438453
"Parser %s is either not part of %s or not a callable"
439454
% (func, path)
440455
)
441-
return parser(val)
456+
return_value = parser(val)
457+
if not is_plain and not isinstance(return_value, Mapping):
458+
raise ValueError(
459+
"Parser %s returned a value that is not a mapping (got type %s): %s"
460+
% (str(parser), type(return_value), return_value)
461+
)
462+
return return_value
442463

443464

444465
class LocalFileInput(click.Path):
@@ -474,23 +495,22 @@ def config_options_with_config_input(cmd):
474495
# List all the configuration options
475496
for arg in parameters[::-1]:
476497
kwargs = arg.option_kwargs(False)
477-
if arg.name.lower() in config_seen:
498+
if arg.name in config_seen:
478499
msg = (
479-
"Multiple configurations use the same name '%s'. Note that names are "
480-
"case-insensitive. Please change the "
500+
"Multiple configurations use the same name '%s'. Please change the "
481501
"names of some of your configurations" % arg.name
482502
)
483503
raise MetaflowException(msg)
484-
config_seen.add(arg.name.lower())
504+
config_seen.add(arg.name)
485505
if kwargs["required"]:
486506
required_names.append(arg.name)
487507

488-
defaults[arg.name.lower()] = (
508+
defaults[arg.name] = (
489509
arg.kwargs.get("default", None),
490510
arg._default_is_file,
491511
)
492-
help_strs.append(" - %s: %s" % (arg.name.lower(), kwargs.get("help", "")))
493-
parsers[arg.name.lower()] = arg.parser
512+
help_strs.append(" - %s: %s" % (arg.name, kwargs.get("help", "")))
513+
parsers[arg.name] = (arg.parser, arg.kwargs["plain"])
494514

495515
if not config_seen:
496516
# No configurations -- don't add anything; we set it to False so that it

metaflow/user_configs/config_parameters.py

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -379,8 +379,10 @@ def __call__(self, ctx=None, deploy_time=False):
379379
to_eval_expr,
380380
self._globals or globals(),
381381
{
382-
k: ConfigValue(v)
383-
for k, v in flow_cls._flow_state[FlowStateItems.CONFIGS].items()
382+
k: v if plain_flag else ConfigValue(v)
383+
for k, (v, plain_flag) in flow_cls._flow_state[
384+
FlowStateItems.CONFIGS
385+
].items()
384386
},
385387
)
386388
except NameError as e:
@@ -467,6 +469,13 @@ class Config(Parameter, collections.abc.Mapping):
467469
If the name starts with a ".", it is assumed to be relative to "metaflow".
468470
show_default : bool, default True
469471
If True, show the default value in the help text.
472+
plain : bool, default False
473+
If True, the configuration value is just returned as is and not converted to
474+
a ConfigValue. Use this is you just want to directly access your configuration.
475+
Note that modifications are not persisted across steps (ie: ConfigValue prevents
476+
modifications and raises and error -- if you have your own object, no error
477+
is raised but no modifications are persisted). You can also use this to return
478+
any arbitrary object (not just dictionary-like objects).
470479
"""
471480

472481
IS_CONFIG_PARAMETER = True
@@ -485,6 +494,7 @@ def __init__(
485494
help: Optional[str] = None,
486495
required: Optional[bool] = None,
487496
parser: Optional[Union[str, Callable[[str], Dict[Any, Any]]]] = None,
497+
plain: Optional[bool] = False,
488498
**kwargs: Dict[str, str]
489499
):
490500
if default is not None and default_value is not None:
@@ -494,6 +504,7 @@ def __init__(
494504
)
495505
self._default_is_file = default is not None
496506
kwargs["default"] = default if default is not None else default_value
507+
kwargs["plain"] = plain
497508
super(Config, self).__init__(
498509
name, required=required, help=help, type=str, **kwargs
499510
)
@@ -507,22 +518,20 @@ def __init__(
507518
self._delayed_evaluator = None
508519

509520
def load_parameter(self, v):
510-
if v is None:
511-
return None
512-
return ConfigValue(v)
521+
return v if v is None or self._kwargs["plain"] else ConfigValue(v)
513522

514523
def _store_value(self, v: Any) -> None:
515524
self._computed_value = v
516525

517526
def _init_delayed_evaluator(self) -> None:
518527
if self._delayed_evaluator is None:
519-
self._delayed_evaluator = DelayEvaluator(self.name.lower())
528+
self._delayed_evaluator = DelayEvaluator(self.name)
520529

521530
# Support <config>.<var> syntax
522531
def __getattr__(self, name):
523532
# Need to return a new DelayEvaluator everytime because the evaluator will
524533
# contain the "path" (ie: .name) and can be further accessed.
525-
return getattr(DelayEvaluator(self.name.lower()), name)
534+
return getattr(DelayEvaluator(self.name), name)
526535

527536
# Next three methods are to implement mapping to support **<config> syntax. We
528537
# need to be careful, however, to also support a regular `config["key"]` syntax
@@ -539,7 +548,7 @@ def __getitem__(self, key):
539548
self._init_delayed_evaluator()
540549
if isinstance(key, str) and key.startswith(UNPACK_KEY):
541550
return self._delayed_evaluator[key]
542-
return DelayEvaluator(self.name.lower())[key]
551+
return DelayEvaluator(self.name)[key]
543552

544553

545554
def resolve_delayed_evaluator(

metaflow/user_decorators/mutable_flow.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,8 +114,10 @@ def start(self):
114114
from metaflow.flowspec import FlowStateItems
115115

116116
# When configs are parsed, they are loaded in _flow_state[FlowStateItems.CONFIGS]
117-
for name, value in self._flow_cls._flow_state[FlowStateItems.CONFIGS].items():
118-
r = name, ConfigValue(value)
117+
for name, (value, plain_flag) in self._flow_cls._flow_state[
118+
FlowStateItems.CONFIGS
119+
].items():
120+
r = name, value if plain_flag else ConfigValue(value)
119121
debug.userconf_exec("Mutable flow yielding config: %s" % str(r))
120122
yield r
121123

0 commit comments

Comments
 (0)