Repository: holoviz/panel

Issue:
Make loading_indicator a valid config parameter
Hi

I really love the `defer_load` and `loading_indicator` options. They make my app so much more professionally looking.

But `defer_load` is actually a bit easier to turn on because you can use it via `pn.config` and `pn.extension`. You cannot do that for `loading_indicator`.

```python
import panel as pn

pn.extension(
    throttled=True, # Only update plots when the mouse is released
    defer_load=True, # Only update plots once the application has loaded
    loading_indicator=True, # Show a loading indicator while the plot is updating. NOT SUPPORTED YET
)
```

```bash
AttributeError: 'loading_indicator' is not a valid config parameter.
Traceback (most recent call last):
  File "/home/jovyan/conda-envs/era5/lib/python3.10/site-packages/bokeh/application/handlers/code_runner.py", line 231, in run
    exec(self._code, module.__dict__)
  File "/home/jovyan/repos/era5-python-app/app.py", line 6, in <module>
    pn.extension(
  File "/home/jovyan/conda-envs/era5/lib/python3.10/site-packages/pyviz_comms/__init__.py", line 64, in __new__
    return param.ParameterizedFunction.__new__(cls, *args, **kwargs)
  File "/home/jovyan/conda-envs/era5/lib/python3.10/site-packages/param/parameterized.py", line 3658, in __new__
    return inst.__call__(*args,**params)
  File "/home/jovyan/conda-envs/era5/lib/python3.10/site-packages/panel/config.py", line 570, in __call__
    setattr(config, k, v)
  File "/home/jovyan/conda-envs/era5/lib/python3.10/site-packages/panel/config.py", line 314, in __setattr__
    raise AttributeError(f'{attr!r} is not a valid config parameter.')
AttributeError: 'loading_indicator' is not a valid config parameter.
```

Please make `loading_indicator` a global option.


Discussion:

Resolving patch:
diff --git a/panel/config.py b/panel/config.py
--- a/panel/config.py
+++ b/panel/config.py
@@ -121,6 +121,9 @@ class _config(_base_config):
     load_entry_points = param.Boolean(default=True, doc="""
         Load entry points from external packages.""")
 
+    loading_indicator = param.Boolean(default=False, doc="""
+        Whether a loading indicator is shown by default while panes are updating.""")
+
     loading_spinner = param.Selector(default='arc', objects=[
         'arc', 'arcs', 'bar', 'dots', 'petal'], doc="""
         Loading indicator to use when component loading parameter is set.""")
diff --git a/panel/param.py b/panel/param.py
--- a/panel/param.py
+++ b/panel/param.py
@@ -764,7 +764,7 @@ class ParamMethod(ReplacementPane):
     return any object which itself can be rendered as a Pane.
     """
 
-    defer_load = param.Boolean(default=None, doc="""
+    defer_load = param.Boolean(default=config.defer_load, doc="""
         Whether to defer load until after the page is rendered.
         Can be set as parameter or by setting panel.config.defer_load.""")
 
@@ -772,15 +772,11 @@ class ParamMethod(ReplacementPane):
         Whether to lazily evaluate the contents of the object
         only when it is required for rendering.""")
 
-    loading_indicator = param.Boolean(default=False, doc="""
-        Whether to show loading indicator while pane is updating.""")
+    loading_indicator = param.Boolean(default=config.loading_indicator, doc="""
+        Whether to show a loading indicator while the pane is updating.
+        Can be set as parameter or by setting panel.config.loading_indicator.""")
 
     def __init__(self, object=None, **params):
-        if (
-            self.param.defer_load.default is None and
-            'defer_load' not in params and config.defer_load
-        ):
-            params['defer_load'] = config.defer_load
         super().__init__(object, **params)
         self._evaled = not (self.lazy or self.defer_load)
         self._link_object_params()
@@ -920,7 +916,13 @@ def _get_model(
     def applies(cls, obj: Any) -> float | bool | None:
         return inspect.ismethod(obj) and isinstance(get_method_owner(obj), param.Parameterized)
 
+@param.depends(config.param.defer_load, watch=True)
+def _update_defer_load_default(default_value):
+    ParamMethod.param.defer_load.default = default_value
 
+@param.depends(config.param.loading_indicator, watch=True)
+def _update_loading_indicator_default(default_value):
+    ParamMethod.param.loading_indicator.default = default_value
 
 class ParamFunction(ParamMethod):
     """


Test patch:
diff --git a/panel/tests/test_param.py b/panel/tests/test_param.py
--- a/panel/tests/test_param.py
+++ b/panel/tests/test_param.py
@@ -10,6 +10,7 @@
     TextInput as BkTextInput, Toggle,
 )
 
+from panel import config
 from panel.io.state import set_curdoc, state
 from panel.layout import Row, Tabs
 from panel.pane import (
@@ -1095,6 +1096,60 @@ def view(a):
     assert pane._models == {}
     assert inner_pane._models == {}
 
+class ParameterizedMock(param.Parameterized):
+        run = param.Event()
+
+        @param.depends("run")
+        def click_view(self):
+            return "click..."
+
+def test_param_function_pane_config_defer_load():
+    # When
+    app = ParameterizedMock()
+    test = ParamMethod(app.click_view)
+    # Then
+    assert test.defer_load==config.defer_load
+
+    # When
+    config.defer_load=not config.defer_load
+    app = ParameterizedMock()
+    test = ParamMethod(app.click_view)
+    # Then
+    assert test.defer_load==config.defer_load
+
+    # When
+    config.defer_load=not config.defer_load
+    app = ParameterizedMock()
+    test = ParamMethod(app.click_view)
+    # Then
+    assert test.defer_load==config.defer_load
+
+    # Clean Up
+    config.defer_load=config.param.defer_load.default
+
+def test_param_function_pane_config_loading_indicator():
+    # When
+    app = ParameterizedMock()
+    test = ParamMethod(app.click_view)
+    # Then
+    assert test.loading_indicator==config.loading_indicator
+
+    # When
+    config.loading_indicator=not config.loading_indicator
+    app = ParameterizedMock()
+    test = ParamMethod(app.click_view)
+    # Then
+    assert test.loading_indicator==config.loading_indicator
+
+    # When
+    config.loading_indicator=not config.loading_indicator
+    app = ParameterizedMock()
+    test = ParamMethod(app.click_view)
+    # Then
+    assert test.loading_indicator==config.loading_indicator
+
+    # Clean Up
+    config.loading_indicator=config.param.loading_indicator.default
 
 def test_param_function_pane_update(document, comm):
     test = View()