Repository: bokeh/bokeh

Issue:
Tile provider causes "Model must be owned by only a single document" error
**System**:
Bokeh 1.0.4
python 3.6
Ubuntu 18.04

#### Description of expected behavior and the observed behavior

**Expected:** When using a `tile_provider` such as `CARTODBPOSITRON`, the object that is imported globally can be attached to more than one document.
**Actual:** A RuntimeError is raised, because the tile provider already is part of a document.

#### Complete, minimal, self-contained example code that reproduces the issue

```python
from bokeh.tile_providers import CARTODBPOSITRON
from bokeh.plotting import figure
from bokeh.io import show


def test():
    fig = figure(plot_width=1000, plot_height=1000,
                 active_scroll="wheel_zoom",
                 x_range=(49., 49.5), y_range=(30., 30.5),
                 x_axis_type="mercator", y_axis_type="mercator",
                 )

    fig.add_tile(CARTODBPOSITRON)
    show(fig)

test()
test()
```

Traceback:

```
Traceback (most recent call last):
  File "/home/veith/.PyCharmCE2018.3/config/scratches/scratch.py", line 18, in <module>
    test()
  File "/home/veith/.PyCharmCE2018.3/config/scratches/scratch.py", line 15, in test
    show(fig)
  File "/home/veith/vscen/lib/python3.6/site-packages/bokeh/io/showing.py", line 142, in show
    return _show_with_state(obj, state, browser, new, notebook_handle=notebook_handle)
  File "/home/veith/vscen/lib/python3.6/site-packages/bokeh/io/showing.py", line 180, in _show_with_state
    _show_file_with_state(obj, state, new, controller)
  File "/home/veith/vscen/lib/python3.6/site-packages/bokeh/io/showing.py", line 163, in _show_file_with_state
    filename = save(obj, state=state)
  File "/home/veith/vscen/lib/python3.6/site-packages/bokeh/io/saving.py", line 86, in save
    _save_helper(obj, filename, resources, title, template)
  File "/home/veith/vscen/lib/python3.6/site-packages/bokeh/io/saving.py", line 148, in _save_helper
    html = file_html(obj, resources, title=title, template=template)
  File "/home/veith/vscen/lib/python3.6/site-packages/bokeh/embed/standalone.py", line 307, in file_html
    with OutputDocumentFor(models, apply_theme=theme, always_new=_always_new) as doc:
  File "/home/veith/Tools/pyenv/versions/3.6.5/lib/python3.6/contextlib.py", line 81, in __enter__
    return next(self.gen)
  File "/home/veith/vscen/lib/python3.6/site-packages/bokeh/embed/util.py", line 137, in OutputDocumentFor
    doc.add_root(model)
  File "/home/veith/vscen/lib/python3.6/site-packages/bokeh/document/document.py", line 304, in add_root
    self._pop_all_models_freeze()
  File "/home/veith/vscen/lib/python3.6/site-packages/bokeh/document/document.py", line 1018, in _pop_all_models_freeze
    self._recompute_all_models()
  File "/home/veith/vscen/lib/python3.6/site-packages/bokeh/document/document.py", line 1041, in _recompute_all_models
    a._attach_document(self)
  File "/home/veith/vscen/lib/python3.6/site-packages/bokeh/model.py", line 609, in _attach_document
    raise RuntimeError("Models must be owned by only a single document, %r is already in a doc" % (self))
RuntimeError: Models must be owned by only a single document, WMTSTileSource(id='1001', ...) is already in a doc
```

Moving the import into the function solves the issue:

```python
from bokeh.plotting import figure
from bokeh.io import show


def test():
    from bokeh.tile_providers import CARTODBPOSITRON  # Importing here works

    fig = figure(plot_width=1000, plot_height=1000,
                 active_scroll="wheel_zoom",
                 x_range=(49., 49.5), y_range=(30., 30.5),
                 x_axis_type="mercator", y_axis_type="mercator",
                 )

    fig.add_tile(CARTODBPOSITRON)

    show(fig)

test()
test()
```

This is neither very intuitive, nor mentioned in the [documentation](https://bokeh.pydata.org/en/latest/docs/reference/tile_providers.html) (as far as I have seen).

Tile provider causes "Model must be owned by only a single document" error
**System**:
Bokeh 1.0.4
python 3.6
Ubuntu 18.04

#### Description of expected behavior and the observed behavior

**Expected:** When using a `tile_provider` such as `CARTODBPOSITRON`, the object that is imported globally can be attached to more than one document.
**Actual:** A RuntimeError is raised, because the tile provider already is part of a document.

#### Complete, minimal, self-contained example code that reproduces the issue

```python
from bokeh.tile_providers import CARTODBPOSITRON
from bokeh.plotting import figure
from bokeh.io import show


def test():
    fig = figure(plot_width=1000, plot_height=1000,
                 active_scroll="wheel_zoom",
                 x_range=(49., 49.5), y_range=(30., 30.5),
                 x_axis_type="mercator", y_axis_type="mercator",
                 )

    fig.add_tile(CARTODBPOSITRON)
    show(fig)

test()
test()
```

Traceback:

```
Traceback (most recent call last):
  File "/home/veith/.PyCharmCE2018.3/config/scratches/scratch.py", line 18, in <module>
    test()
  File "/home/veith/.PyCharmCE2018.3/config/scratches/scratch.py", line 15, in test
    show(fig)
  File "/home/veith/vscen/lib/python3.6/site-packages/bokeh/io/showing.py", line 142, in show
    return _show_with_state(obj, state, browser, new, notebook_handle=notebook_handle)
  File "/home/veith/vscen/lib/python3.6/site-packages/bokeh/io/showing.py", line 180, in _show_with_state
    _show_file_with_state(obj, state, new, controller)
  File "/home/veith/vscen/lib/python3.6/site-packages/bokeh/io/showing.py", line 163, in _show_file_with_state
    filename = save(obj, state=state)
  File "/home/veith/vscen/lib/python3.6/site-packages/bokeh/io/saving.py", line 86, in save
    _save_helper(obj, filename, resources, title, template)
  File "/home/veith/vscen/lib/python3.6/site-packages/bokeh/io/saving.py", line 148, in _save_helper
    html = file_html(obj, resources, title=title, template=template)
  File "/home/veith/vscen/lib/python3.6/site-packages/bokeh/embed/standalone.py", line 307, in file_html
    with OutputDocumentFor(models, apply_theme=theme, always_new=_always_new) as doc:
  File "/home/veith/Tools/pyenv/versions/3.6.5/lib/python3.6/contextlib.py", line 81, in __enter__
    return next(self.gen)
  File "/home/veith/vscen/lib/python3.6/site-packages/bokeh/embed/util.py", line 137, in OutputDocumentFor
    doc.add_root(model)
  File "/home/veith/vscen/lib/python3.6/site-packages/bokeh/document/document.py", line 304, in add_root
    self._pop_all_models_freeze()
  File "/home/veith/vscen/lib/python3.6/site-packages/bokeh/document/document.py", line 1018, in _pop_all_models_freeze
    self._recompute_all_models()
  File "/home/veith/vscen/lib/python3.6/site-packages/bokeh/document/document.py", line 1041, in _recompute_all_models
    a._attach_document(self)
  File "/home/veith/vscen/lib/python3.6/site-packages/bokeh/model.py", line 609, in _attach_document
    raise RuntimeError("Models must be owned by only a single document, %r is already in a doc" % (self))
RuntimeError: Models must be owned by only a single document, WMTSTileSource(id='1001', ...) is already in a doc
```

Moving the import into the function solves the issue:

```python
from bokeh.plotting import figure
from bokeh.io import show


def test():
    from bokeh.tile_providers import CARTODBPOSITRON  # Importing here works

    fig = figure(plot_width=1000, plot_height=1000,
                 active_scroll="wheel_zoom",
                 x_range=(49., 49.5), y_range=(30., 30.5),
                 x_axis_type="mercator", y_axis_type="mercator",
                 )

    fig.add_tile(CARTODBPOSITRON)

    show(fig)

test()
test()
```

This is neither very intuitive, nor mentioned in the [documentation](https://bokeh.pydata.org/en/latest/docs/reference/tile_providers.html) (as far as I have seen).



Discussion:
This was reported on two previous occasions (see https://github.com/bokeh/bokeh/issues/4770 and https://github.com/bokeh/bokeh/issues/6590), which were both closed. I do strongly agree that this is unexpected and unintuitive. I'm not sure what the best solution is but providing already instantiated models does not seem like the best way to ship these default tile sources.
As a quick fix I'd suggest at least adding a comment in the documentation. (sorry for the duplicate, but I thought if the issues are closed this should be a new issue). 
> This was reported on two previous occasions (see #4770 and #6590), which were both closed. I do strongly agree that this is unexpected and unintuitive. I'm not sure what the best solution is but providing already instantiated models does not seem like the best way to ship these default tile sources.
 
#4770 was closed because it only referred to this problem in the context of Bokeh server apps, and that problem was solved by using the "module hack" to always return a new instance of the object on import or access, and that works for server apps because each session does an import. But that obviously does not help in instances like this where there is only one import. 

As for workarounds, I would also expect this to work:
```
import bokeh.tile_providers as tp

def test():
    ...
    fig.add_tile(tp.CARTODBPOSITRON)
```
I'd be amenable to adding functions for generating built in tile provider instances, changing all the examples to use the functions, and deprecating the direct attribute access (which we can easily do since they go through a getter already). Obviously executing that deprecation is Bokeh 2.0 timeframe. 
That would make sense to me. Is there anything I should do for this?
@RunOrVeith If you have the ability to submit a PR for this, it would absolutely be appreciated. I think given the current situation vs where we would like to go will make it a little awkward but not too bad. I'd propose this:

* make a new method `get_provider(self, provider_name)` that can return a new instance of a tile provider when called like `get_provider(STAMEN_TONER_BACKGROUND)`
* in the future (2.0) `STAMEN_TONER_BACKGROUND`, etc, can be string consts or enums. For *right now*, since they are properties, `get_provider` will have to switch on the method objects themselves to return the right new instance (that's the slightly unusual bit)
* the current properties should also get 2.0 depreciation warnings with explicit advice to use `get_provider`

If you'd like to look into it and need more information, please let us know
This was reported on two previous occasions (see https://github.com/bokeh/bokeh/issues/4770 and https://github.com/bokeh/bokeh/issues/6590), which were both closed. I do strongly agree that this is unexpected and unintuitive. I'm not sure what the best solution is but providing already instantiated models does not seem like the best way to ship these default tile sources.
As a quick fix I'd suggest at least adding a comment in the documentation. (sorry for the duplicate, but I thought if the issues are closed this should be a new issue). 
> This was reported on two previous occasions (see #4770 and #6590), which were both closed. I do strongly agree that this is unexpected and unintuitive. I'm not sure what the best solution is but providing already instantiated models does not seem like the best way to ship these default tile sources.
 
#4770 was closed because it only referred to this problem in the context of Bokeh server apps, and that problem was solved by using the "module hack" to always return a new instance of the object on import or access, and that works for server apps because each session does an import. But that obviously does not help in instances like this where there is only one import. 

As for workarounds, I would also expect this to work:
```
import bokeh.tile_providers as tp

def test():
    ...
    fig.add_tile(tp.CARTODBPOSITRON)
```
I'd be amenable to adding functions for generating built in tile provider instances, changing all the examples to use the functions, and deprecating the direct attribute access (which we can easily do since they go through a getter already). Obviously executing that deprecation is Bokeh 2.0 timeframe. 
That would make sense to me. Is there anything I should do for this?
@RunOrVeith If you have the ability to submit a PR for this, it would absolutely be appreciated. I think given the current situation vs where we would like to go will make it a little awkward but not too bad. I'd propose this:

* make a new method `get_provider(self, provider_name)` that can return a new instance of a tile provider when called like `get_provider(STAMEN_TONER_BACKGROUND)`
* in the future (2.0) `STAMEN_TONER_BACKGROUND`, etc, can be string consts or enums. For *right now*, since they are properties, `get_provider` will have to switch on the method objects themselves to return the right new instance (that's the slightly unusual bit)
* the current properties should also get 2.0 depreciation warnings with explicit advice to use `get_provider`

If you'd like to look into it and need more information, please let us know

Resolving patch:
diff --git a/bokeh/tile_providers.py b/bokeh/tile_providers.py
--- a/bokeh/tile_providers.py
+++ b/bokeh/tile_providers.py
@@ -9,9 +9,42 @@
 
 Attributes:
 
+    .. bokeh-enum:: Vendors
+        :module: bokeh.tile_providers
+
+    get_provider
+        Use this function to retrieve an instance of a predefined tile provider.
+
+        Args:
+            provider_name (Union[str, Vendors])
+                Name of the tile provider to supply.
+                Use tile_providers.Vendors enum, or the name of a provider as string
+
+        Returns:
+            WMTSTileProviderSource: The desired tile provider instance
+
+        Raises:
+            ValueError, if the provider can not be found
+            ValueError, if the attribution for that provider can not be found
+
+
+        Example:
+
+            .. code-block:: python
+
+                    >>> from bokeh.tile_providers import get_provider, Vendors
+                    >>> get_provider(Vendors.CARTODBPOSITRON)
+                    <class 'bokeh.models.tiles.WMTSTileSource'>
+                    >>> get_provider('CARTODBPOSITRON')
+                    <class 'bokeh.models.tiles.WMTSTileSource'>
+
+
     CARTODBPOSITRON
         Tile Source for CartoDB Tile Service
 
+        Warns:
+            BokehDeprecationWarning: Deprecated in bokeh 1.1.0. Use get_provider instead
+
         .. raw:: html
 
             <img src="https://tiles.basemaps.cartocdn.com/light_all/14/2627/6331.png" />
@@ -19,6 +52,9 @@
     CARTODBPOSITRON_RETINA
         Tile Source for CartoDB Tile Service (tiles at 'retina' resolution)
 
+        Warns:
+            BokehDeprecationWarning: Deprecated in bokeh 1.1.0. Use get_provider instead
+
         .. raw:: html
 
             <img src="https://tiles.basemaps.cartocdn.com/light_all/14/2627/6331@2x.png" />
@@ -26,6 +62,9 @@
     STAMEN_TERRAIN
         Tile Source for Stamen Terrain Service
 
+        Warns:
+            BokehDeprecationWarning: Deprecated in bokeh 1.1.0. Use get_provider instead
+
         .. raw:: html
 
             <img src="http://c.tile.stamen.com/terrain/14/2627/6331.png" />
@@ -33,6 +72,9 @@
     STAMEN_TERRAIN_RETINA
         Tile Source for Stamen Terrain Service
 
+        Warns:
+            BokehDeprecationWarning: Deprecated in bokeh 1.1.0. Use get_provider instead
+
         .. raw:: html
 
             <img src="http://c.tile.stamen.com/terrain/14/2627/6331@2x.png" />
@@ -40,6 +82,9 @@
     STAMEN_TONER
         Tile Source for Stamen Toner Service
 
+        Warns:
+            BokehDeprecationWarning: Deprecated in bokeh 1.1.0. Use get_provider instead
+
         .. raw:: html
 
             <img src="http://c.tile.stamen.com/toner/14/2627/6331.png" />
@@ -47,6 +92,9 @@
     STAMEN_TONER_BACKGROUND
         Tile Source for Stamen Toner Background Service which does not include labels
 
+        Warns:
+            BokehDeprecationWarning: Deprecated in bokeh 1.1.0. Use get_provider instead
+
         .. raw:: html
 
             <img src="http://c.tile.stamen.com/toner-background/14/2627/6331.png" />
@@ -54,6 +102,9 @@
     STAMEN_TONER_LABELS
         Tile Source for Stamen Toner Service which includes only labels
 
+        Warns:
+            BokehDeprecationWarning: Deprecated in bokeh 1.1.0. Use get_provider instead
+
         .. raw:: html
 
             <img src="http://c.tile.stamen.com/toner-labels/14/2627/6331.png" />
@@ -71,6 +122,7 @@
 from __future__ import absolute_import, division, print_function, unicode_literals
 
 import logging
+
 log = logging.getLogger(__name__)
 
 #-----------------------------------------------------------------------------
@@ -84,6 +136,8 @@
 # External imports
 
 # Bokeh imports
+from bokeh.core.enums import enumeration
+
 
 #-----------------------------------------------------------------------------
 # Globals and constants
@@ -103,8 +157,18 @@
 # Private API
 #-----------------------------------------------------------------------------
 
-class _TileProvidersModule(types.ModuleType):
+# Can be removed in bokeh 2.0
+def _make_deprecated_property(name):
+
+    def deprecated_property_tile(self):
+        from bokeh.util.deprecation import deprecated
+        deprecated(since_or_msg=(1, 1, 0), old=name, new='get_provider(Vendors.%s)' % name)
+        return self.get_provider(provider_name=name)
+    func = property(deprecated_property_tile)
+    return func
 
+
+class _TileProvidersModule(types.ModuleType):
     _CARTO_ATTRIBUTION = (
         '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors,'
         '&copy; <a href="https://cartodb.com/attributions">CartoDB</a>'
@@ -117,63 +181,61 @@ class _TileProvidersModule(types.ModuleType):
         'under %s.'
     )
 
-    # Properties --------------------------------------------------------------
+    _SERVICE_URLS = dict(
+        CARTODBPOSITRON='https://tiles.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png',
+        CARTODBPOSITRON_RETINA='https://tiles.basemaps.cartocdn.com/light_all/{z}/{x}/{y}@2x.png',
+        STAMEN_TERRAIN='http://tile.stamen.com/terrain/{Z}/{X}/{Y}.png',
+        STAMEN_TERRAIN_RETINA='http://tile.stamen.com/terrain/{Z}/{X}/{Y}@2x.png',
+        STAMEN_TONER='http://tile.stamen.com/toner/{Z}/{X}/{Y}.png',
+        STAMEN_TONER_BACKGROUND='http://tile.stamen.com/toner-background/{Z}/{X}/{Y}.png',
+        STAMEN_TONER_LABELS='http://tile.stamen.com/toner-labels/{Z}/{X}/{Y}.png',
+    )
 
-    @property
-    def CARTODBPOSITRON(self):
-        from bokeh.models.tiles import WMTSTileSource
-        return WMTSTileSource(
-            url='https://tiles.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png',
-            attribution=self._CARTO_ATTRIBUTION
-        )
+    _STAMEN_ATTRIBUTION_URLS = dict(
+        STAMEN_TERRAIN='<a href="https://creativecommons.org/licenses/by-sa/3.0">CC BY SA</a>',
+        STAMEN_TERRAIN_RETINA='<a href="https://creativecommons.org/licenses/by-sa/3.0">CC BY SA</a>',
+        STAMEN_TONER='<a href="https://www.openstreetmap.org/copyright">ODbL</a>',
+        STAMEN_TONER_BACKGROUND='<a href="https://www.openstreetmap.org/copyright">ODbL</a>',
+        STAMEN_TONER_LABELS='<a href="https://www.openstreetmap.org/copyright">ODbL</a>',
+    )
 
-    @property
-    def CARTODBPOSITRON_RETINA(self):
-        from bokeh.models.tiles import WMTSTileSource
-        return WMTSTileSource(
-            url='https://tiles.basemaps.cartocdn.com/light_all/{z}/{x}/{y}@2x.png',
-            attribution=self._CARTO_ATTRIBUTION
-        )
+    Vendors = enumeration('CARTODBPOSITRON', 'CARTODBPOSITRON_RETINA',
+                          'STAMEN_TERRAIN', 'STAMEN_TERRAIN_RETINA', 'STAMEN_TONER',
+                          'STAMEN_TONER_BACKGROUND', 'STAMEN_TONER_LABELS',
+                          case_sensitive=True)
 
-    @property
-    def STAMEN_TERRAIN(self):
+    def get_provider(self, provider_name):
         from bokeh.models.tiles import WMTSTileSource
-        return WMTSTileSource(
-            url='http://tile.stamen.com/terrain/{Z}/{X}/{Y}.png',
-            attribution=self._STAMEN_ATTRIBUTION % '<a href="https://creativecommons.org/licenses/by-sa/3.0">CC BY SA</a>'
-        )
 
-    @property
-    def STAMEN_TERRAIN_RETINA(self):
-        from bokeh.models.tiles import WMTSTileSource
-        return WMTSTileSource(
-            url='http://tile.stamen.com/terrain/{Z}/{X}/{Y}@2x.png',
-            attribution=self._STAMEN_ATTRIBUTION % '<a href="https://creativecommons.org/licenses/by-sa/3.0">CC BY SA</a>'
-        )
+        if isinstance(provider_name, WMTSTileSource):
+            # This allows `get_provider(CARTODBPOSITRON)` to work
+            return WMTSTileSource(url=provider_name.url, attribution=provider_name.attribution)
 
-    @property
-    def STAMEN_TONER(self):
-        from bokeh.models.tiles import WMTSTileSource
-        return WMTSTileSource(
-            url='http://tile.stamen.com/toner/{Z}/{X}/{Y}.png',
-            attribution=self._STAMEN_ATTRIBUTION % '<a href="https://www.openstreetmap.org/copyright">ODbL</a>'
-        )
+        selected_provider = provider_name.upper()
 
-    @property
-    def STAMEN_TONER_BACKGROUND(self):
-        from bokeh.models.tiles import WMTSTileSource
-        return WMTSTileSource(
-            url='http://tile.stamen.com/toner-background/{Z}/{X}/{Y}.png',
-            attribution=self._STAMEN_ATTRIBUTION % '<a href="https://www.openstreetmap.org/copyright">ODbL</a>'
-        )
+        if selected_provider not in self.Vendors:
+            raise ValueError('Unknown tile provider %s' % provider_name)
+
+        url = self._SERVICE_URLS[selected_provider]
+        if selected_provider.startswith('CARTO'):
+            attribution = self._CARTO_ATTRIBUTION
+        elif selected_provider.startswith('STAMEN'):
+            attribution = self._STAMEN_ATTRIBUTION % self._STAMEN_ATTRIBUTION_URLS[selected_provider]
+        else:
+            raise ValueError('Can not retrieve attribution for %s' % selected_provider)
+        return WMTSTileSource(url=url, attribution=attribution)
+
+    # Properties --------------------------------------------------------------
+
+    # For bokeh 2.0 these can easily be replaced with their corresponding enum values
+    CARTODBPOSITRON = _make_deprecated_property(Vendors.CARTODBPOSITRON)
+    CARTODBPOSITRON_RETINA = _make_deprecated_property(Vendors.CARTODBPOSITRON_RETINA)
+    STAMEN_TERRAIN = _make_deprecated_property(Vendors.STAMEN_TERRAIN)
+    STAMEN_TERRAIN_RETINA = _make_deprecated_property(Vendors.STAMEN_TERRAIN_RETINA)
+    STAMEN_TONER = _make_deprecated_property(Vendors.STAMEN_TONER)
+    STAMEN_TONER_BACKGROUND = _make_deprecated_property(Vendors.STAMEN_TONER_BACKGROUND)
+    STAMEN_TONER_LABELS = _make_deprecated_property(Vendors.STAMEN_TONER_LABELS)
 
-    @property
-    def STAMEN_TONER_LABELS(self):
-        from bokeh.models.tiles import WMTSTileSource
-        return WMTSTileSource(
-            url='http://tile.stamen.com/toner-labels/{Z}/{X}/{Y}.png',
-            attribution=self._STAMEN_ATTRIBUTION % '<a href="https://www.openstreetmap.org/copyright">ODbL</a>'
-        )
 
 #-----------------------------------------------------------------------------
 # Code
@@ -189,6 +251,8 @@ def STAMEN_TONER_LABELS(self):
     'STAMEN_TONER',
     'STAMEN_TONER_BACKGROUND',
     'STAMEN_TONER_LABELS',
+    'get_provider',
+    'Vendors'
 )
 sys.modules['bokeh.tile_providers'] = _mod
-del _mod, sys, types
+del _mod, sys, types, _make_deprecated_property
diff --git a/examples/integration/tiles/tile_smoothing.py b/examples/integration/tiles/tile_smoothing.py
--- a/examples/integration/tiles/tile_smoothing.py
+++ b/examples/integration/tiles/tile_smoothing.py
@@ -1,6 +1,6 @@
 from bokeh.layouts import column
 from bokeh.plotting import figure, show, output_file
-from bokeh.tile_providers import CARTODBPOSITRON
+from bokeh.tile_providers import get_provider, Vendors
 
 output_file("tile_smoothing.html")
 
@@ -14,9 +14,10 @@
 p2.y_range = p1.y_range
 
 # Add smoothed tiles (the default)
-r1 = p1.add_tile(CARTODBPOSITRON)
+tile_provider = get_provider(Vendors.CARTODBPOSITRON)
+r1 = p1.add_tile(tile_provider)
 
 # Add non-smoothed tile
-r2 = p2.add_tile(CARTODBPOSITRON, smoothing=False)
+r2 = p2.add_tile(tile_provider, smoothing=False)
 
 show(column(p1, p2))
diff --git a/examples/plotting/file/tile_source.py b/examples/plotting/file/tile_source.py
--- a/examples/plotting/file/tile_source.py
+++ b/examples/plotting/file/tile_source.py
@@ -1,11 +1,11 @@
 from bokeh.plotting import figure, show, output_file
-from bokeh.tile_providers import CARTODBPOSITRON
+from bokeh.tile_providers import get_provider, Vendors
 
 output_file("tile_source.html")
 
 # create plot and add tools
 p = figure(x_range=(-2000000, 2000000), y_range=(1000000, 7000000),
            x_axis_type="mercator", y_axis_type="mercator")
-p.add_tile(CARTODBPOSITRON)
+p.add_tile(get_provider(Vendors.CARTODBPOSITRON))
 
 show(p)


Test patch:
diff --git a/bokeh/tests/test_tile_providers.py b/bokeh/tests/test_tile_providers.py
--- a/bokeh/tests/test_tile_providers.py
+++ b/bokeh/tests/test_tile_providers.py
@@ -39,6 +39,8 @@
     'STAMEN_TONER',
     'STAMEN_TONER_BACKGROUND',
     'STAMEN_TONER_LABELS',
+    'get_provider',
+    'Vendors'
 )
 
 _CARTO_URLS = {
@@ -72,15 +74,20 @@
 @pytest.mark.unit
 class Test_StamenProviders(object):
     def test_type(self, name):
-        p = getattr(bt, name)
+        with pytest.deprecated_call():
+            p = getattr(bt, name)
         assert isinstance(p, WMTSTileSource)
 
     def test_url(self, name):
-        p = getattr(bt, name)
+        with pytest.deprecated_call():
+            p = getattr(bt, name)
         assert p.url == _STAMEN_URLS[name]
 
     def test_attribution(self, name):
-        p = getattr(bt, name)
+        with pytest.deprecated_call():
+            p = getattr(bt, name)
+
+        print(p.attribution)
         assert p.attribution == (
             'Map tiles by <a href="https://stamen.com">Stamen Design</a>, '
             'under <a href="https://creativecommons.org/licenses/by/3.0">CC BY 3.0</a>. '
@@ -89,32 +96,72 @@ def test_attribution(self, name):
         ) % _STAMEN_LIC[name]
 
     def test_copies(self, name):
-        p1 = getattr(bt, name)
-        p2 = getattr(bt, name)
+        with pytest.deprecated_call():
+            p1 = getattr(bt, name)
+            p2 = getattr(bt, name)
         assert p1 is not p2
 
 @pytest.mark.parametrize('name', ['CARTODBPOSITRON', 'CARTODBPOSITRON_RETINA'])
 @pytest.mark.unit
 class Test_CartoProviders(object):
     def test_type(self, name):
-        p = getattr(bt, name)
+        with pytest.deprecated_call():
+            p = getattr(bt, name)
         assert isinstance(p, WMTSTileSource)
 
     def test_url(self, name):
-        p = getattr(bt, name)
+        with pytest.deprecated_call():
+            p = getattr(bt, name)
         assert p.url == _CARTO_URLS[name]
 
     def test_attribution(self, name):
-        p = getattr(bt, name)
+        with pytest.deprecated_call():
+            p = getattr(bt, name)
         assert p.attribution == (
             '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors,'
             '&copy; <a href="https://cartodb.com/attributions">CartoDB</a>'
         )
 
     def test_copies(self, name):
-        p1 = getattr(bt, name)
-        p2 = getattr(bt, name)
+        with pytest.deprecated_call():
+            p1 = getattr(bt, name)
+            p2 = getattr(bt, name)
+        assert p1 is not p2
+
+
+@pytest.mark.unit
+class Test_GetProvider(object):
+
+    @pytest.mark.parametrize('name', ['CARTODBPOSITRON', 'CARTODBPOSITRON_RETINA', 'STAMEN_TERRAIN',
+                                      'STAMEN_TERRAIN_RETINA', 'STAMEN_TONER', 'STAMEN_TONER_BACKGROUND',
+                                      'STAMEN_TONER_LABELS', ])
+    def test_get_provider(self, name):
+        assert name in bt.Vendors
+        enum_member = getattr(bt.Vendors, name)
+        p1 = bt.get_provider(enum_member)
+        p2 = bt.get_provider(name)
+        p3 = bt.get_provider(name.lower())
+        assert isinstance(p1, WMTSTileSource)
+        assert isinstance(p2, WMTSTileSource)
+        assert isinstance(p3, WMTSTileSource)
         assert p1 is not p2
+        assert p2 is not p3
+        assert p1 is not p3
+        assert p1.url == p2.url == p3.url
+        assert p1.attribution == p2.attribution == p3.attribution
+
+        with pytest.deprecated_call():
+            # This will not return a WMTSTileSource in bokeh 2.0.0!
+            default_instance = getattr(bt, name)
+        new_instance = bt.get_provider(default_instance)
+        assert default_instance is not new_instance
+        assert default_instance.url == new_instance.url
+        assert default_instance.attribution == new_instance.attribution
+
+    def test_unknown_vendor(self):
+        with pytest.raises(ValueError):
+            bt.get_provider("This is not a valid tile vendor")
+
 
 #-----------------------------------------------------------------------------
 # Dev API