Repository: dask/distributed

Issue:
Numeric environment variable configuration is broken
I recently found that a pipeline was throwing this error:
```
Traceback (most recent call last):
  File "/usr/local/lib/python3.6/site-packages/distributed/utils.py", line 623, in log_errors
    yield
  File "/usr/local/lib/python3.6/site-packages/distributed/scheduler.py", line 1591, in remove_worker
    if ts.suspicious > self.allowed_failures:
TypeError: '>' not supported between instances of 'int' and 'str'
```
The environment variable `DASK_ALLOWED_FAILURES=20` had been set. In a new terminal I ran the following:
```pya
>>> LocalCluster().scheduler.allowed_failures
'20'
```
There appears to be nothing in the code which ensures that the type of a configuration variable conforms to the expected type.

I'd be happy to submit a PR to fix this but I'm not really sure what form it should take. Some ideas:
1. Remove environment variable configuration completely (it doesn't seem to be documented?)
2. Use `pyyaml` to interpret environment variables the same way yaml interprets them
    - (introduces are hard dependency on `pyyaml`)
3. Approximate the above by just automatically casting all env var config variables to ints where possible
4. Make a `get_config` which casts its return value to be the same type as the provided default; replace `config.get` everywhere. e.g.
```py
def get_config(key, default=None):
    if default is not None:
        return type(default)(config.get(key, default=default))
    return config.get(key)
```
5. Change the `config` dict to be an instance of a subclass of dict which has `get()` overridden to behave as suggested above
6. Change `config.py` to use the example `yaml` file to correctly cast all config variables
7. Cast variables correctly at the point of `get` everywhere
    - e.g. `ALLOWED_FAILURES = int(config.get('allowed-failures', 3))`

I think I'd probably go with 3, which seems like the lowest impact solution.

Of course, the workaround for now is to just use a yaml configuration file.



Discussion:
Yes, this seems like a good summary of the problem and of possible solutions.  I agree that option 3 is a nice place to start.  I might add the request that we also support floats, perhaps with an approach like the following:

```python
def parse(x):
    try:
        x = float(x)
    except ValueError:
        return x
    if int(x) == x:
        return int(x)
    else:
        return x
```

> I'd be happy to submit a PR to fix this ...

Thank you for being willing to help here.  That would be quite welcome.

I also wouldn't be surprised if we eventually establish a more thorough configuration system.  Until then this would be a very welcome addition.

Resolving patch:
diff --git a/distributed/config.py b/distributed/config.py
--- a/distributed/config.py
+++ b/distributed/config.py
@@ -67,7 +67,24 @@ def load_env_vars(config):
     for name, value in os.environ.items():
         if name.startswith('DASK_'):
             varname = name[5:].lower().replace('_', '-')
-            config[varname] = value
+            config[varname] = _parse_env_value(value)
+
+
+def _parse_env_value(value):
+    """ Convert a string to an integer, float or boolean (in that order) if possible. """
+    bools = {
+        'true': True,
+        'false': False
+    }
+    try:
+        return int(value)
+    except ValueError:
+        pass
+    try:
+        return float(value)
+    except ValueError:
+        pass
+    return bools.get(value.lower(), value)
 
 
 def _initialize_logging_old_style(config):


Test patch:
diff --git a/distributed/tests/test_config.py b/distributed/tests/test_config.py
--- a/distributed/tests/test_config.py
+++ b/distributed/tests/test_config.py
@@ -9,8 +9,8 @@
 import pytest
 
 from distributed.utils_test import (captured_handler, captured_logger,
-                                    new_config, new_config_file)
-from distributed.config import initialize_logging, set_config, config
+                                    new_config, new_config_file, new_environment)
+from distributed.config import initialize_logging, set_config, config, load_env_vars
 
 
 def dump_logger_list():
@@ -279,3 +279,28 @@ def test_set_config():
     with set_config(foo=1):
         assert config['foo'] == 1
     assert 'foo' not in config
+
+
+def test_load_env_vars():
+    environment = dict(
+        DASK_STRING='test',
+        DASK_INT='20',
+        DASK_TRUE='True',
+        DASK_FALSE='false',
+        DASK_FLOAT='1.5',
+        NOT_FOR_DASK='__variable not used__'
+    )
+    conf = {}
+    with new_environment(environment):
+        load_env_vars(conf)
+        assert conf['string'] == 'test'
+        assert conf['int'] == 20
+        assert conf['true'] is True
+        assert conf['false'] is False
+        assert conf['float'] == 1.5
+        assert isinstance(conf['string'], str)
+        assert isinstance(conf['int'], int)
+        assert isinstance(conf['float'], float)
+        assert isinstance(conf['true'], bool)
+        assert isinstance(conf['false'], bool)
+        assert '__variable not used__' not in conf.values()
diff --git a/distributed/utils_test.py b/distributed/utils_test.py
--- a/distributed/utils_test.py
+++ b/distributed/utils_test.py
@@ -1066,6 +1066,17 @@ def new_config(new_config):
         initialize_logging(config)
 
 
+@contextmanager
+def new_environment(changes):
+    saved_environ = os.environ.copy()
+    os.environ.update(changes)
+    try:
+        yield
+    finally:
+        os.environ.clear()
+        os.environ.update(saved_environ)
+
+
 @contextmanager
 def new_config_file(c):
     """