Skip to content

Commit 5ac661e

Browse files
mauvilsatimsnyder
andauthored
Add support for single json dict environment variable for protocol (#1194)
* Add support for single json dict environment variable for protocol. * Update docs/source/features.rst Co-authored-by: Tim Snyder <[email protected]> * Add warnings when environment variables are ignored. --------- Co-authored-by: Tim Snyder <[email protected]>
1 parent c75410b commit 5ac661e

File tree

3 files changed

+93
-14
lines changed

3 files changed

+93
-14
lines changed

docs/source/features.rst

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -365,14 +365,16 @@ For instance:
365365
Obviously, you should only define default values that are appropriate for
366366
a given file system implementation. INI files only support string values.
367367

368-
Alternatively, you can provide overrides with environment variables of
369-
the style ``FSSPEC_{protocol}_{kwargname}=value``.
368+
Alternatively, you can provide overrides with environment variables of the style
369+
``FSSPEC_{protocol}=<json_dict_value>`` and
370+
``FSSPEC_{protocol}_{kwargname}=<string_value>``.
370371

371372
Configuration is determined in the following order, with later items winning:
372373

373-
#. the contents of ini files, and json files in the config directory, sorted
374-
alphabetically
375-
#. environment variables
374+
#. ini and json files in the config directory (``FSSPEC_CONFIG_DIRECTORY`` or ``$HOME/.config/fsspec/``), sorted
375+
lexically by filename
376+
#. ``FSSPEC_{protocol}`` environment variables
377+
#. ``FSSPEC_{protocol}_{kwargname}`` environment variables
376378
#. the contents of ``fsspec.config.conf``, which can be edited at runtime
377379
#. kwargs explicitly passed, whether with ``fsspec.open``, ``fsspec.filesystem``
378380
or directly instantiating the implementation class.

fsspec/config.py

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import configparser
22
import json
33
import os
4+
import warnings
45

56
conf = {}
67
default_conf_dir = os.path.join(os.path.expanduser("~"), ".config/fsspec")
@@ -10,9 +11,14 @@
1011
def set_conf_env(conf_dict, envdict=os.environ):
1112
"""Set config values from environment variables
1213
13-
Looks for variable of the form ``FSSPEC_<protocol>_<kwarg>``.
14-
There is no attempt to convert strings, but the kwarg keys will
15-
be lower-cased.
14+
Looks for variables of the form ``FSSPEC_<protocol>`` and
15+
``FSSPEC_<protocol>_<kwarg>``. For ``FSSPEC_<protocol>`` the value is parsed
16+
as a json dictionary and used to ``update`` the config of the
17+
corresponding protocol. For ``FSSPEC_<protocol>_<kwarg>`` there is no
18+
attempt to convert the string value, but the kwarg keys will be lower-cased.
19+
20+
The ``FSSPEC_<protocol>_<kwarg>`` variables are applied after the
21+
``FSSPEC_<protocol>`` ones.
1622
1723
Parameters
1824
----------
@@ -21,12 +27,35 @@ def set_conf_env(conf_dict, envdict=os.environ):
2127
envdict : dict-like(str, str)
2228
Source for the values - usually the real environment
2329
"""
30+
kwarg_keys = []
2431
for key in envdict:
25-
if key.startswith("FSSPEC"):
26-
if key.count("_") < 2:
32+
if key.startswith("FSSPEC_") and len(key) > 7 and key[7] != "_":
33+
if key.count("_") > 1:
34+
kwarg_keys.append(key)
2735
continue
28-
_, proto, kwarg = key.split("_", 2)
29-
conf_dict.setdefault(proto.lower(), {})[kwarg.lower()] = envdict[key]
36+
try:
37+
value = json.loads(envdict[key])
38+
except json.decoder.JSONDecodeError as ex:
39+
warnings.warn(
40+
f"Ignoring environment variable {key} due to a parse failure: {ex}"
41+
)
42+
else:
43+
if isinstance(value, dict):
44+
_, proto = key.split("_", 1)
45+
conf_dict.setdefault(proto.lower(), {}).update(value)
46+
else:
47+
warnings.warn(
48+
f"Ignoring environment variable {key} due to not being a dict:"
49+
f" {type(value)}"
50+
)
51+
elif key.startswith("FSSPEC"):
52+
warnings.warn(
53+
f"Ignoring environment variable {key} due to having an unexpected name"
54+
)
55+
56+
for key in kwarg_keys:
57+
_, proto, kwarg = key.split("_", 2)
58+
conf_dict.setdefault(proto.lower(), {})[kwarg.lower()] = envdict[key]
3059

3160

3261
def set_conf_files(cdir, conf_dict):

fsspec/tests/test_config.py

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import os
2+
from warnings import catch_warnings
23

34
import pytest
45

@@ -14,17 +15,64 @@ def clean_conf():
1415
conf.clear()
1516

1617

17-
def test_from_env(clean_conf):
18+
def test_from_env_ignored(clean_conf):
19+
env = {
20+
"FSSPEC": "missing_protocol",
21+
"FSSPEC_": "missing_protocol",
22+
"FSSPEC__INVALID_KEY": "invalid_protocol",
23+
"FSSPEC_INVALID1": "not_json_dict",
24+
"FSSPEC_INVALID2": '["not_json_dict"]',
25+
}
26+
cd = {}
27+
with catch_warnings(record=True) as w:
28+
set_conf_env(conf_dict=cd, envdict=env)
29+
assert len(w) == 5
30+
assert "unexpected name" in str(w[0].message)
31+
assert "unexpected name" in str(w[1].message)
32+
assert "unexpected name" in str(w[2].message)
33+
assert "parse failure" in str(w[3].message)
34+
assert "not being a dict" in str(w[4].message)
35+
assert cd == {}
36+
37+
38+
def test_from_env_kwargs(clean_conf):
1839
env = {
1940
"FSSPEC_PROTO_KEY": "value",
2041
"FSSPEC_PROTO_LONG_KEY": "othervalue",
2142
"FSSPEC_MALFORMED": "novalue",
2243
}
2344
cd = {}
24-
set_conf_env(conf_dict=cd, envdict=env)
45+
with catch_warnings(record=True) as w:
46+
set_conf_env(conf_dict=cd, envdict=env)
47+
assert len(w) == 1
48+
assert "parse failure" in str(w[0].message)
2549
assert cd == {"proto": {"key": "value", "long_key": "othervalue"}}
2650

2751

52+
def test_from_env_protocol_dict(clean_conf):
53+
env = {
54+
"FSSPEC_PROTO": '{"int": 1, "float": 2.3, "bool": true, "dict": {"key": "val"}}'
55+
}
56+
cd = {}
57+
set_conf_env(conf_dict=cd, envdict=env)
58+
assert cd == {
59+
"proto": {"int": 1, "float": 2.3, "bool": True, "dict": {"key": "val"}}
60+
}
61+
62+
63+
def test_from_env_kwargs_override_protocol_dict(clean_conf):
64+
env = {
65+
"FSSPEC_PROTO_LONG_KEY": "override1",
66+
"FSSPEC_PROTO": '{"key": "value1", "long_key": "value2", "otherkey": "value3"}',
67+
"FSSPEC_PROTO_KEY": "override2",
68+
}
69+
cd = {}
70+
set_conf_env(conf_dict=cd, envdict=env)
71+
assert cd == {
72+
"proto": {"key": "override2", "long_key": "override1", "otherkey": "value3"}
73+
}
74+
75+
2876
def test_from_file_ini(clean_conf, tmpdir):
2977
file1 = os.path.join(tmpdir, "1.ini")
3078
file2 = os.path.join(tmpdir, "2.ini")

0 commit comments

Comments
 (0)