Repository: dask/dask

Issue:
Deprecation warning is pretty intense
```python
In [1]: import dask

In [2]: import dask.dataframe
<ipython-input-2-1d704cbd4e0a>:1: DeprecationWarning: The current Dask DataFrame implementation is deprecated.
In a future release, Dask DataFrame will use new implementation that
contains several improvements including a logical query planning.
The user-facing DataFrame API will remain unchanged.

The new implementation is already available and can be enabled by
installing the dask-expr library:

    $ pip install dask-expr

and turning the query planning option on:

    >>> import dask
    >>> dask.config.set({'dataframe.query-planning': True})
    >>> import dask.dataframe as dd

API documentation for the new implementation is available at
https://docs.dask.org/en/stable/dask-expr-api.html

Any feedback can be reported on the Dask issue tracker
https://github.com/dask/dask/issues

  import dask.dataframe

```

Also this happens every time I import `dask.dataframe`, rather than just once.  Is there a good way to turn it off?  This strikes me as scary.

In my mind if we're giving people a scary warning then there needs to be an easy way to opt-out of that warning, either by opting in to the new behavior (if we think that that's wise) or by having an opt-out option for future use

```
To disable this warning in the future, run the following command:

dask config set dataframe.query-optimization-warning=False
```

Discussion:
milesgranger: Thanks for the review @crusaderky, clearly the CLI will take a bit more work, so I've moved the config option over to https://github.com/dask/dask/pull/10925. 

> The new command dumps out your whole dask config to ~/.config/dask/dask.yaml.

Indeed, I wasn't sure if this was correct either but saw this is the same flow being done with `coiled config set` and thus followed suit. And will anyway address the remainder later today. 
milesgranger: > Could you document the new command in docs/source/configuration.rst?

Ah nuts, sorry I'd forgotten that, done now.

![image](https://github.com/dask/dask/assets/13764397/dcc9a7b6-a3a5-42a9-ba61-c06a894ec7e1)


Patch:
diff --git a/dask/cli.py b/dask/cli.py
index 5ec92e209..f28ea1f33 100644
--- a/dask/cli.py
+++ b/dask/cli.py
@@ -1,11 +1,12 @@
 from __future__ import annotations
 
+import pathlib
 import warnings
 from functools import reduce
 
 import click
 import importlib_metadata
-from yaml import dump
+import yaml
 
 import dask
 from dask import __version__
@@ -67,7 +68,7 @@ def config_get(key=None):
         try:
             data = reduce(lambda d, k: d[k], key.split("."), dask.config.config)
             if isinstance(data, (list, dict)):
-                click.echo_via_pager(dump(data))
+                click.echo_via_pager(yaml.dump(data))
             else:
                 click.echo(data)
         except KeyError:
@@ -75,10 +76,65 @@ def config_get(key=None):
             exit(1)
 
 
+@config.command(name="set")
+@click.argument("key", default=None, required=False)
+@click.argument("value", default=None, required=False)
+def config_set(key=None, value=None):
+    """Set a Dask config key to a new value"""
+    if key in (None, ""):
+        click.echo(
+            click.style(
+                """Config key not specified. Are you looking for "dask config list"?"""
+            ),
+            err=True,
+        )
+        exit(1)
+    else:
+        value = dask.config.interpret_value(value)
+        _, path = save_config({key: value})
+        click.echo(f"Updated [{key}] to [{value}], config saved to {path}")
+
+
+def save_config(new_config: dict) -> tuple[dict, pathlib.Path]:
+    """
+    Save new config values to dask config file,
+    return new config and path to file.
+    """
+    config_file = pathlib.Path(dask.config.PATH).joinpath("dask.yaml")
+    config_file.parent.mkdir(exist_ok=True, parents=True)
+    if config_file.exists():
+        with open(str(config_file)) as f:
+            config = yaml.safe_load(f) or dict()
+    else:
+        config = dict()
+
+    dask.config.set(new_config, config=config)
+
+    try:
+        config_file.write_text(yaml.dump(config))
+    except OSError as e:
+        raise RuntimeError(
+            f"""
+
+For some reason we couldn't write config to {config_file}.
+Perhaps you don't have permissions here?
+
+You can change the directory where Dask writes config files
+to somewhere else using the DASK_CONFIG environment variable.
+
+For example, you could set the following:
+
+    export DASK_CONFIG=~/.dask
+"""
+        ) from e
+
+    return config, config_file
+
+
 @config.command(name="list")
 def config_list():
     """Print the whole config."""
-    click.echo_via_pager(dump(dask.config.config))
+    click.echo_via_pager(yaml.dump(dask.config.config))
 
 
 def _register_command_ep(interface, entry_point):
diff --git a/dask/config.py b/dask/config.py
index ed30ab08f..d5a70a475 100644
--- a/dask/config.py
+++ b/dask/config.py
@@ -257,19 +257,23 @@ def collect_env(env: Mapping[str, str] | None = None) -> dict:
     for name, value in env.items():
         if name.startswith("DASK_"):
             varname = name[5:].lower().replace("__", ".")
-            try:
-                d[varname] = ast.literal_eval(value)
-            except (SyntaxError, ValueError):
-                if value.lower() in ("none", "null"):
-                    d[varname] = None
-                else:
-                    d[varname] = value
+            d[varname] = interpret_value(value)
 
     result: dict = {}
     set(d, config=result)
     return result
 
 
+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
+
+
 def ensure_file(
     source: str, destination: str | None = None, comment: bool = True
 ) -> None:
diff --git a/dask/dask-schema.yaml b/dask/dask-schema.yaml
index a9ce92e80..20412201c 100644
--- a/dask/dask-schema.yaml
+++ b/dask/dask-schema.yaml
@@ -93,6 +93,10 @@ properties:
         type: [boolean]
         description: |
           Whether to use query planning.
+      query-planning-warning:
+        type: [boolean]
+        description: |
+          Whether to warn about future use of query planning.
 
   array:
     type: object
diff --git a/dask/dask.yaml b/dask/dask.yaml
index ac6030061..3b264f152 100644
--- a/dask/dask.yaml
+++ b/dask/dask.yaml
@@ -16,6 +16,7 @@ dataframe:
     metadata-task-size-remote: 1  # Number of files per remote metadata-processing task
   convert-string: null  # Whether to convert string-like data to pyarrow strings
   query-planning: false  # Whether to use dask-expr
+  query-planning-warning: true  # Warn about future query optimization change
 
 array:
   backend: "numpy"  # Backend array library for input IO and data creation
diff --git a/dask/dataframe/__init__.py b/dask/dataframe/__init__.py
index fca52eba9..b68398ab8 100644
--- a/dask/dataframe/__init__.py
+++ b/dask/dataframe/__init__.py
@@ -159,9 +159,11 @@ else:
             '  python -m pip install "dask[dataframe]" --upgrade  # or python -m pip install'
         )
         raise ImportError(msg) from e
-    warnings.warn(
-        """The current Dask DataFrame implementation is deprecated. 
-In a future release, Dask DataFrame will use new implementation that
+
+    if dask.config.get("dataframe.query-planning-warning"):
+        warnings.warn(
+            """The current Dask DataFrame implementation is deprecated. 
+In a future release, Dask DataFrame will use a new implementation that
 contains several improvements including a logical query planning.
 The user-facing DataFrame API will remain unchanged.
 
@@ -181,10 +183,14 @@ https://docs.dask.org/en/stable/dask-expr-api.html
 
 Any feedback can be reported on the Dask issue tracker
 https://github.com/dask/dask/issues 
+
+To disable this warning in the future, set dask config:
+
+    >>> dask.config.set({'dataframe.query-planning-warning': False})
 """,
-        DeprecationWarning,
-        stacklevel=2,
-    )
+            DeprecationWarning,
+            stacklevel=2,
+        )
 
 
 from dask.dataframe._testing import test_dataframe
diff --git a/docs/source/configuration.rst b/docs/source/configuration.rst
index 8369a3a3c..e61857db0 100644
--- a/docs/source/configuration.rst
+++ b/docs/source/configuration.rst
@@ -186,6 +186,19 @@ they are initialized. These settings may also be cached for performance reasons.
 This is particularly true for ``dask.distributed`` objects such as Client, Scheduler,
 Worker, and Nanny.
 
+Directly from CLI
+~~~~~~~~~~~~~~~~~
+
+Configuration can also be set and viewed from the CLI.
+
+.. code-block::
+
+	$ dask config set optimization.fuse.ave-width 4
+	Updated [optimization.fuse.ave-width] to [4], config saved to ~/dask/dask.yaml
+
+	$ dask config get optimization.fuse.ave-width
+	4
+
 
 Distributing configuration
 ~~~~~~~~~~~~~~~~~~~~~~~~~~


Test patch:
diff --git a/dask/tests/test_cli.py b/dask/tests/test_cli.py
index 11f4ea102..b38b1c133 100644
--- a/dask/tests/test_cli.py
+++ b/dask/tests/test_cli.py
@@ -1,12 +1,14 @@
 from __future__ import annotations
 
 import json
+import pathlib
 import platform
 import sys
 
 import click
 import importlib_metadata
 import pytest
+import yaml
 from click.testing import CliRunner
 
 import dask
@@ -34,6 +36,43 @@ def test_config_get_bad_value():
     assert result.output.startswith("Section not found")
 
 
+@pytest.fixture
+def tmp_conf_dir(tmpdir):
+    original = dask.config.PATH
+    dask.config.PATH = str(tmpdir)
+    try:
+        yield tmpdir
+    finally:
+        dask.config.PATH = original
+
+
+@pytest.mark.parametrize("empty_config", (True, False))
+@pytest.mark.parametrize("value", ("333MiB", 2, [1, 2], {"foo": "bar"}))
+def test_config_set_value(tmp_conf_dir, value, empty_config):
+    config_file = pathlib.Path(tmp_conf_dir) / "dask.yaml"
+
+    if not empty_config:
+        expected_conf = {"dataframe": {"foo": "bar"}}
+        config_file.write_text(yaml.dump(expected_conf))
+    else:
+        expected_conf = dict()
+        assert not config_file.exists()
+
+    runner = CliRunner()
+    result = runner.invoke(
+        dask.cli.config_set, ["array.chunk-size", str(value)], catch_exceptions=False
+    )
+
+    expected = (
+        f"Updated [array.chunk-size] to [{value}], config saved to {config_file}\n"
+    )
+    assert expected == result.output
+
+    actual_conf = yaml.safe_load(config_file.read_text())
+    expected_conf.update({"array": {"chunk-size": value}})
+    assert expected_conf == actual_conf
+
+
 def test_config_list():
     runner = CliRunner()
     result = runner.invoke(dask.cli.config_list)