Repository: bokeh/bokeh

Issue:
BOKEH_RESOURCES does not work with server
As of 1.4 the server currently ignores e.g. 

    BOKEH_RESOURCES=cdn

The reason for this is that previously, setting resources like that actually modified all Resources objects to simply override their specified operation. So although the server hard codes "server" resources:

    return Resources(mode="server", root_url=root_url, path_versioner=StaticHandler.append_version)

Things still "worked". However with the latest settings updates, this surreptitious modification no longer happens. The server code should be updated to actually inpect `settings.resources` and act accordingly. 


Discussion:
Noting that this mostly affects the demo site, as CDN  resources were specified as a means to reduce our costs by utilizing less ELB bandwidth. I think it will be OK to leave as-is until 2.0 but if there is a spike in cost we may revert to 1.3.4 (some tweaking/better tagging of our dockerhub will be necessary)
I also noticed that development resources have been finicky to use recently. I suppose both issues are connected.
@mattpap can you elaborate? I don't really use `BOKEH_DEV` so I don't know what it's supposed to do. But if you mention specific requirements I can make sure they are in place when I fix this. 

Resolving patch:
diff --git a/bokeh/server/django/consumers.py b/bokeh/server/django/consumers.py
--- a/bokeh/server/django/consumers.py
+++ b/bokeh/server/django/consumers.py
@@ -40,6 +40,7 @@
 from bokeh.server.session import ServerSession
 from bokeh.server.views.static_handler import StaticHandler
 from bokeh.server.protocol_handler import ProtocolHandler
+from bokeh.settings import settings
 from bokeh.protocol import Protocol
 from bokeh.protocol.receiver import Receiver
 from bokeh.protocol.message import Message
@@ -79,8 +80,11 @@ def get_argument(self, name: str, default: Optional[str] = None) -> Optional[str
         return self.arguments.get(name, default)
 
     def resources(self, absolute_url: Optional[str] = None) -> Resources:
-        root_url = urljoin(absolute_url, self._prefix) if absolute_url else self._prefix
-        return Resources(mode="server", root_url=root_url, path_versioner=StaticHandler.append_version)
+        mode = settings.resources(default="server")
+        if mode == "server":
+            root_url = urljoin(absolute_url, self._prefix) if absolute_url else self._prefix
+            return Resources(mode="server", root_url=root_url, path_versioner=StaticHandler.append_version)
+        return Resources(mode=mode)
 
 class SessionConsumer(AsyncHttpConsumer, ConsumerHelper):
 
diff --git a/bokeh/server/tornado.py b/bokeh/server/tornado.py
--- a/bokeh/server/tornado.py
+++ b/bokeh/server/tornado.py
@@ -21,6 +21,7 @@
 # Standard library imports
 import os
 from pprint import pformat
+from urllib.parse import urljoin
 
 # External imports
 from tornado import gen
@@ -437,8 +438,11 @@ def resources(self, absolute_url=None):
                 relative URLs are used (default: None)
 
         '''
-        root_url = absolute_url + self._prefix if absolute_url else self._prefix
-        return Resources(mode="server", root_url=root_url, path_versioner=StaticHandler.append_version)
+        mode = settings.resources(default="server")
+        if mode == "server":
+            root_url = urljoin(absolute_url, self._prefix) if absolute_url else self._prefix
+            return Resources(mode="server", root_url=root_url, path_versioner=StaticHandler.append_version)
+        return Resources(mode=mode)
 
     def start(self):
         ''' Start the Bokeh Server application.
diff --git a/bokeh/settings.py b/bokeh/settings.py
--- a/bokeh/settings.py
+++ b/bokeh/settings.py
@@ -77,10 +77,19 @@
     Future support is planned to load Bokeh settings from global system
     configurations.
 
-implicit defaults
-    These are default values defined by the setting declarations. There are
-    regular defaults, as well as "dev" defaults that are used when the
-    environment variable ``BOKEH_DEV=yes`` is set.
+local defaults
+    These are default values defined when accessing the setting:
+
+    .. code-block:: python
+
+        settings.resources(default="server")
+
+    Local defaults have lower precendence than every other setting mechanism
+    except global defaults.
+
+global defaults
+    These are default values defined by the setting declarations. They have
+    lower precedence than every other setting mechanism.
 
 If no value is obtained after searching all of these locations, then a
 RuntimeError will be raised.
@@ -244,13 +253,14 @@ class PrioritizedSetting(object):
 
     The following methods are searched in order for the setting:
 
-    6. immediately supplied values
-    5. previously user-set values (e.g. set from command line)
-    4. user-specified config override file
-    3. environment variable
-    2. local user config file
-    1. global system config file (not yet implemented)
-    0. implicit defaults
+    7. immediately supplied values
+    6. previously user-set values (e.g. set from command line)
+    5. user-specified config override file
+    4. environment variable
+    3. local user config file
+    2. global system config file (not yet implemented)
+    1. local defaults
+    0. global defaults
 
     Ref: https://stackoverflow.com/a/11077282/3406693
 
@@ -277,7 +287,7 @@ def __init__(self, name, env_var=None, default=_Unset, dev_default=_Unset, conve
         self._parent = None
         self._user_value = _Unset
 
-    def __call__(self, value=None):
+    def __call__(self, value=None, default=_Unset):
         '''Return the setting value according to the standard precedence.
 
         Args:
@@ -285,6 +295,10 @@ def __call__(self, value=None):
                 An optional immediate value. If not None, the value will
                 be converted, then returned.
 
+            default (any, optional):
+                An optional default value that only takes precendence over
+                implicit default values specified on the property itself.
+
         Returns:
             str or int or float
 
@@ -292,33 +306,39 @@ def __call__(self, value=None):
             RuntimeError
         '''
 
-        # 6. immediate values
+        # 7. immediate values
         if value is not None:
             return self._convert(value)
 
-        # 5. previously user-set value
+        # 6. previously user-set value
         if self._user_value is not _Unset:
             return self._convert(self._user_value)
 
-        # 4. user-named config file
+        # 5. user-named config file
         if self._parent and self._name in self._parent.config_override:
             return self._convert(self._parent.config_override[self._name])
 
-        # 3. environment variable
+        # 4. environment variable
         if self._env_var and self._env_var in os.environ:
             return self._convert(os.environ[self._env_var])
 
-        # 2. local config file
+        # 3. local config file
         if self._parent and self._name in self._parent.config_user:
             return self._convert(self._parent.config_user[self._name])
 
-        # 1. global config file
+        # 2. global config file
         if self._parent and self._name in self._parent.config_system:
             return self._convert(self._parent.config_system[self._name])
 
-        # 0. implicit defaults
+        # 1.5 (undocumented) dev defaults take precedence over other defaults
         if is_dev() and self._dev_default is not _Unset:
             return self._convert(self._dev_default)
+
+        # 1. local defaults
+        if default is not _Unset:
+            return self._convert(default)
+
+        # 0. global defaults
         if self._default is not _Unset:
             return self._convert(self._default)
 


Test patch:
diff --git a/tests/unit/bokeh/server/test_tornado__server.py b/tests/unit/bokeh/server/test_tornado__server.py
--- a/tests/unit/bokeh/server/test_tornado__server.py
+++ b/tests/unit/bokeh/server/test_tornado__server.py
@@ -17,6 +17,7 @@
 # Standard library imports
 import json
 import logging
+import os
 
 # External imports
 
@@ -79,6 +80,23 @@ def test_default_resources():
         assert r.root_url == "/foo/bar/"
         assert r.path_versioner == StaticHandler.append_version
 
+def test_env_resources():
+    os.environ['BOKEH_RESOURCES'] = 'cdn'
+    application = Application()
+    with ManagedServerLoop(application) as server:
+        r = server._tornado.resources()
+        assert r.mode == "cdn"
+    del os.environ['BOKEH_RESOURCES']
+
+def test_dev_resources():
+    os.environ['BOKEH_DEV'] = 'yes'
+    application = Application()
+    with ManagedServerLoop(application) as server:
+        r = server._tornado.resources()
+        assert r.mode == "absolute"
+        assert r.dev
+    del os.environ['BOKEH_DEV']
+
 def test_index():
     application = Application()
     with ManagedServerLoop(application) as server:
diff --git a/tests/unit/bokeh/test_settings.py b/tests/unit/bokeh/test_settings.py
--- a/tests/unit/bokeh/test_settings.py
+++ b/tests/unit/bokeh/test_settings.py
@@ -184,16 +184,22 @@ def test_name(self):
         ps = bs.PrioritizedSetting("foo", env_var="BOKEH_FOO", default=10)
         assert ps.name == "foo"
 
-    def test_default(self):
+    def test_global_default(self):
         ps = bs.PrioritizedSetting("foo", env_var="BOKEH_FOO", default=10)
         assert ps.default == 10
         assert ps() == 10
 
+    def test_local_default(self):
+        ps = bs.PrioritizedSetting("foo", env_var="BOKEH_FOO", default=10)
+        assert ps.default == 10
+        assert ps(default=20) == 20
+
     def test_dev_default(self):
-        ps = bs.PrioritizedSetting("foo", env_var="BOKEH_FOO", default=10, dev_default=20)
-        assert ps.dev_default == 20
+        ps = bs.PrioritizedSetting("foo", env_var="BOKEH_FOO", default=10, dev_default=25)
+        assert ps.dev_default == 25
         os.environ['BOKEH_DEV'] = "yes"
-        assert ps() == 20
+        assert ps() == 25
+        assert ps(default=20) == 25
         del os.environ['BOKEH_DEV']
 
     def test_env_var(self):
@@ -201,6 +207,7 @@ def test_env_var(self):
         ps = bs.PrioritizedSetting("foo", env_var="BOKEH_FOO")
         assert ps.env_var == "BOKEH_FOO"
         assert ps() == "30"
+        assert ps(default=20) == "30"
         del os.environ["BOKEH_FOO"]
 
     def test_env_var_converts(self):
@@ -213,6 +220,7 @@ def test_user_set(self):
         ps = bs.PrioritizedSetting("foo")
         ps.set_value(40)
         assert ps() == 40
+        assert ps(default=20) == 40
 
     def test_user_unset(self):
         ps = bs.PrioritizedSetting("foo", default=2)
@@ -229,6 +237,7 @@ def test_user_set_converts(self):
     def test_immediate(self):
         ps = bs.PrioritizedSetting("foo")
         assert ps(50) == 50
+        assert ps(50, default=20) == 50
 
     def test_immediate_converts(self):
         ps = bs.PrioritizedSetting("foo", convert=int)
@@ -240,39 +249,48 @@ class FakeSettings(object):
             config_user = {}
             config_system = {}
 
-        ps = bs.PrioritizedSetting("foo", env_var="BOKEH_FOO", convert=int, default=0, dev_default=5)
+        ps = bs.PrioritizedSetting("foo", env_var="BOKEH_FOO", convert=int, default=0, dev_default=15)
         ps._parent = FakeSettings
 
-        # 0. implicit default
+        # 0. global default
         assert ps() == 0
 
-        # 0. implicit default (DEV)
+        # 1. local default
+        assert ps(default=10) == 10
+
+        # 1.5. implicit default (DEV)
         os.environ['BOKEH_DEV'] = "yes"
-        assert ps() == 5
+        assert ps() == 15
         del os.environ['BOKEH_DEV']
 
-        # 1. global config file
-        FakeSettings.config_system['foo'] = 10
-        assert ps() == 10
-
-        # 2. local config file
-        FakeSettings.config_user['foo'] = 20
+        # 2. global config file
+        FakeSettings.config_system['foo'] = 20
         assert ps() == 20
+        assert ps(default=10) == 20
 
-        # 3. environment variable
-        os.environ["BOKEH_FOO"] = "30"
+        # 3. local config file
+        FakeSettings.config_user['foo'] = 30
         assert ps() == 30
+        assert ps(default=10) == 30
 
-        # 4. override config file
-        FakeSettings.config_override['foo'] = 40
+        # 4. environment variable
+        os.environ["BOKEH_FOO"] = "40"
         assert ps() == 40
+        assert ps(default=10) == 40
 
-        # 5. previously user-set value
-        ps.set_value(50)
+        # 5. override config file
+        FakeSettings.config_override['foo'] = 50
         assert ps() == 50
+        assert ps(default=10) == 50
+
+        # 6. previously user-set value
+        ps.set_value(60)
+        assert ps() == 60
+        assert ps(default=10) == 60
 
-        # 6. immediate values
-        assert ps(60) == 60
+        # 7. immediate values
+        assert ps(70) == 70
+        assert ps(70, default=10) == 70
 
         del os.environ["BOKEH_FOO"]