Repository: dask/dask
Pull request #10926 (2024-02-15 18:03:13)

Correctly parse lowercase true/false config from CLI
Fix bug, affecting both `dask config set` as well as the parsing environment variables, where a YAML/JSON-style lowercase `true` or `false` would result in a literal string `"true"` instead of a boolean.

To make things worse, most of the code won't actually validate the type of the config variables, so this

```bash
dask config set dataframe.query_planning=false
```
or this
```
export DASK_DATAFRAME__QUERY_PLANNING=false
```
would both evaluate to True because `bool("false") == True`.

github-actions[bot]: ## Unit Test Results
_See [test report](https://dask.github.io/dask/test_report.html) for an extended history of previous test failures. This is useful for diagnosing flaky tests._

     15 files  ±  0       15 suites  ±0   3h 20m 56s :stopwatch: + 3m 47s
 13 051 tests + 11   12 119 :white_check_mark: + 11     931 :zzz: ±0  1 :x: ±0 
161 452 runs  +165  144 916 :white_check_mark: +165  16 535 :zzz: ±0  1 :x: ±0 

For more details on these failures, see [this check](https://github.com/dask/dask/runs/21624341315).

Results for commit 6873b60e. ± Comparison against base commit 0ad38fce.

<details>
  <summary>This pull request <b>removes</b> 1 and <b>adds</b> 12 tests. <i>Note that renamed tests count towards both.</i></summary>

```
dask.tests.test_config ‑ test_env_none_values
```

```
dask.tests.test_config ‑ test_env_special_values[False-False-<lambda>0]
dask.tests.test_config ‑ test_env_special_values[False-False-<lambda>1]
dask.tests.test_config ‑ test_env_special_values[False-False-<lambda>2]
dask.tests.test_config ‑ test_env_special_values[None-None-<lambda>0]
dask.tests.test_config ‑ test_env_special_values[None-None-<lambda>1]
dask.tests.test_config ‑ test_env_special_values[None-None-<lambda>2]
dask.tests.test_config ‑ test_env_special_values[Null-None-<lambda>0]
dask.tests.test_config ‑ test_env_special_values[Null-None-<lambda>1]
dask.tests.test_config ‑ test_env_special_values[Null-None-<lambda>2]
dask.tests.test_config ‑ test_env_special_values[True-True-<lambda>0]
…
```
</details>

[test-results]:data:application/gzip;base64,H4sIAMhZzmUC/02MSQ7DIBAEv2JxzoFhGZt8JmKzhOIlwnCK8vdgEmMfq7pVbzKGyW/k3oG8dWTLIZ3kctQprMvOjEosqqyp7pxKOPixZWvrCUCd8hleRSp+/kYdpv3XhI9xjcXQYmJeahlBSPbnVhZCATb5KwNKLg93SVe+lu06zyEVIDj03CD13qHSWpuea+YGg84aMVhFGaJQbgTy+QKkozCqGQEAAA==


Patch:
diff --git a/dask/cli.py b/dask/cli.py
index f28ea1f33..b9b68828f 100644
--- a/dask/cli.py
+++ b/dask/cli.py
@@ -69,6 +69,8 @@ def config_get(key=None):
             data = reduce(lambda d, k: d[k], key.split("."), dask.config.config)
             if isinstance(data, (list, dict)):
                 click.echo_via_pager(yaml.dump(data))
+            elif data is None:
+                click.echo("None")
             else:
                 click.echo(data)
         except KeyError:
diff --git a/dask/config.py b/dask/config.py
index d5a70a475..b77f1ba61 100644
--- a/dask/config.py
+++ b/dask/config.py
@@ -268,10 +268,11 @@ def interpret_value(value: str) -> Any:
     try:
         return ast.literal_eval(value)
     except (SyntaxError, ValueError):
-        if value.lower() in ("none", "null"):
-            return None
-        else:
-            return value
+        pass
+
+    # Avoid confusion of YAML vs. Python syntax
+    hardcoded_map = {"none": None, "null": None, "false": False, "true": True}
+    return hardcoded_map.get(value.lower(), value)
 
 
 def ensure_file(
diff --git a/dask/dataframe/__init__.py b/dask/dataframe/__init__.py
index b68398ab8..091e28162 100644
--- a/dask/dataframe/__init__.py
+++ b/dask/dataframe/__init__.py
@@ -12,7 +12,7 @@ def _dask_expr_enabled() -> bool:
             import dask_expr  # noqa: F401
         except ImportError:
             raise ValueError("Must install dask-expr to activate query planning.")
-    return use_dask_expr if use_dask_expr is not None else False
+    return use_dask_expr
 
 
 if _dask_expr_enabled():


Test patch:
diff --git a/dask/dataframe/tests/test_rolling.py b/dask/dataframe/tests/test_rolling.py
index bc14ee988..d4c759c58 100644
--- a/dask/dataframe/tests/test_rolling.py
+++ b/dask/dataframe/tests/test_rolling.py
@@ -11,6 +11,9 @@ import dask.dataframe as dd
 from dask.dataframe._compat import PANDAS_GE_210
 from dask.dataframe.utils import assert_eq
 
+DASK_EXPR_ENABLED = dd._dask_expr_enabled()
+
+
 N = 40
 df = pd.DataFrame(
     {
@@ -320,7 +323,7 @@ def test_rolling_cov(window, center):
     assert_eq(prolling.cov(), drolling.cov())
 
 
-@pytest.mark.skipif(dd._dask_expr_enabled(), reason="axis not at all supported")
+@pytest.mark.skipif(DASK_EXPR_ENABLED, reason="axis not at all supported")
 def test_rolling_raises():
     df = pd.DataFrame(
         {"a": np.random.randn(25).cumsum(), "b": np.random.randint(100, size=(25,))}
@@ -346,7 +349,7 @@ def test_rolling_names():
     assert sorted(a.rolling(2).sum().dask) == sorted(a.rolling(2).sum().dask)
 
 
-@pytest.mark.skipif(dd._dask_expr_enabled(), reason="deprecated in pandas")
+@pytest.mark.skipif(DASK_EXPR_ENABLED, reason="deprecated in pandas")
 @pytest.mark.parametrize(
     "kwargs",
     [
@@ -403,14 +406,14 @@ def test_rolling_partition_size():
             dobj.rolling(12).mean().compute()
 
 
-@pytest.mark.skipif(dd._dask_expr_enabled(), reason="different in dask-expr")
+@pytest.mark.skipif(DASK_EXPR_ENABLED, reason="different in dask-expr")
 def test_rolling_repr():
     ddf = dd.from_pandas(pd.DataFrame([10] * 30), npartitions=3)
     res = repr(ddf.rolling(4))
     assert res == "Rolling [window=4,center=False]"
 
 
-@pytest.mark.skipif(dd._dask_expr_enabled(), reason="different in dask-expr")
+@pytest.mark.skipif(DASK_EXPR_ENABLED, reason="different in dask-expr")
 def test_time_rolling_repr():
     res = repr(dts.rolling("4s"))
     assert res == "Rolling [window=4s,center=False,win_type=freq]"
@@ -422,7 +425,7 @@ def test_time_rolling_constructor():
     assert result.min_periods is None
     assert result.win_type is None
 
-    if not dd._dask_expr_enabled():
+    if not DASK_EXPR_ENABLED:
         assert result._win_type == "freq"
 
 
@@ -590,7 +593,7 @@ def test_groupby_rolling():
     assert_eq(expected, actual, check_divisions=False)
 
 
-@pytest.mark.xfail(dd._dask_expr_enabled(), reason="this works in dask-expr")
+@pytest.mark.xfail(DASK_EXPR_ENABLED, reason="this works in dask-expr")
 def test_groupby_rolling_with_integer_window_raises():
     df = pd.DataFrame(
         {"B": [0, 1, 2, np.nan, 4, 5, 6], "C": ["a", "a", "a", "b", "b", "a", "b"]}
diff --git a/dask/tests/test_cli.py b/dask/tests/test_cli.py
index b38b1c133..6224f5121 100644
--- a/dask/tests/test_cli.py
+++ b/dask/tests/test_cli.py
@@ -15,7 +15,7 @@ import dask
 import dask.cli
 
 
-def test_config_get():
+def test_config_get_no_key():
     runner = CliRunner()
     result = runner.invoke(dask.cli.config_get)
     assert result.exit_code == 1
@@ -27,6 +27,7 @@ def test_config_get_value():
     result = runner.invoke(dask.cli.config_get, ["array"])
     assert result.exit_code == 0
     assert result.output.startswith("backend:")
+    assert len(result.output.splitlines()) > 2
 
 
 def test_config_get_bad_value():
@@ -36,6 +37,14 @@ def test_config_get_bad_value():
     assert result.output.startswith("Section not found")
 
 
+def test_config_get_none():
+    with dask.config.set({"foo.bar": None}):
+        runner = CliRunner()
+        result = runner.invoke(dask.cli.config_get, ["foo.bar"])
+        assert result.exit_code == 0
+        assert result.output == "None\n"
+
+
 @pytest.fixture
 def tmp_conf_dir(tmpdir):
     original = dask.config.PATH
diff --git a/dask/tests/test_config.py b/dask/tests/test_config.py
index 6c5bbf272..9300cf681 100644
--- a/dask/tests/test_config.py
+++ b/dask/tests/test_config.py
@@ -242,27 +242,16 @@ def test_env():
     assert res == expected
 
 
-def test_env_none_values():
-    env = {
-        "DASK_A": "None",
-        "DASK_B": "NONE",
-        "DASK_C": "none",
-        "DASK_D": "Null",
-        "DASK_E": "NULL",
-        "DASK_F": "null",
-    }
-
-    expected = {
-        "a": None,
-        "b": None,
-        "c": None,
-        "d": None,
-        "e": None,
-        "f": None,
-    }
-
+@pytest.mark.parametrize(
+    "preproc", [lambda x: x, lambda x: x.lower(), lambda x: x.upper()]
+)
+@pytest.mark.parametrize(
+    "v,out", [("None", None), ("Null", None), ("False", False), ("True", True)]
+)
+def test_env_special_values(preproc, v, out):
+    env = {"DASK_A": preproc(v)}
     res = collect_env(env)
-    assert res == expected
+    assert res == {"a": out}
 
 
 def test_collect():