Repository: bokeh/bokeh

Issue:
[BUG] push_notebook should raise an error if a handle is passed instead of a document
#### ALL software version info (bokeh, python, notebook, OS, browser, any other relevant packages)

Debian GNU/Linux 10 (buster)
Python 3.7 installed in a conda environment
Tested both using Jupyter Lab and Jupyter Notebook

<details>
<summary>environment.yml</summary>

```
name: bokeh-demo
channels:
  - conda-forge
  - defaults
dependencies:
  - _libgcc_mutex=0.1=main
  - arrow-cpp=0.14.1=py37h6b969ab_0
  - asn1crypto=0.24.0=py37_1003
  - attrs=19.1.0=py_0
  - backcall=0.1.0=py_0
  - backports=1.0=py_2
  - backports.weakref=1.0.post1=py37_1000
  - bleach=3.1.0=py_0
  - bokeh=1.3.1=py37_0
  - boost-cpp=1.70.0=h8e57a91_2
  - brotli=1.0.7=he1b5a44_1000
  - bzip2=1.0.8=h516909a_0
  - c-ares=1.15.0=h516909a_1001
  - ca-certificates=2019.6.16=hecc5488_0
  - cachetools=2.1.0=py_0
  - cairo=1.16.0=hfb77d84_1002
  - certifi=2019.6.16=py37_1
  - cffi=1.12.3=py37h8022711_0
  - chardet=3.0.4=py37_1003
  - click=7.0=py_0
  - contextlib2=0.5.5=py_2
  - cryptography=2.7=py37h72c5cf5_0
  - cycler=0.10.0=py_1
  - dbus=1.13.6=he372182_0
  - decorator=4.4.0=py_0
  - defusedxml=0.5.0=py_1
  - docopt=0.6.2=py_1
  - double-conversion=3.1.5=he1b5a44_1
  - entrypoints=0.3=py37_1000
  - expat=2.2.5=he1b5a44_1003
  - flask=1.1.1=py_1
  - fontconfig=2.13.1=h86ecdb6_1001
  - freetype=2.10.0=he983fc9_0
  - fribidi=1.0.5=h516909a_1002
  - future=0.17.1=py37_1000
  - gettext=0.19.8.1=hc5be6a0_1002
  - gflags=2.2.2=he1b5a44_1001
  - glib=2.58.3=h6f030ca_1002
  - glog=0.4.0=he1b5a44_1
  - google-api-core=1.14.2=py37_0
  - google-api-core-grpc=1.14.2=0
  - google-auth=1.6.3=py_0
  - google-cloud-bigquery=1.17.0=py37_0
  - google-cloud-bigtable=0.33.0=py37_0
  - google-cloud-core=1.0.3=py_0
  - google-cloud-datastore=1.9.0=py_0
  - google-resumable-media=0.3.2=py_0
  - googleapis-common-protos=1.6.0=py37_0
  - googleapis-common-protos-grpc=1.6.0=0
  - graphite2=1.3.13=hf484d3e_1000
  - graphviz=2.40.1=h5933667_1
  - grpc-cpp=1.22.0=h18db393_0
  - grpc-google-iam-v1=0.11.4=py37_1
  - grpcio=1.16.1=py37hf8bcb03_1
  - gst-plugins-base=1.14.5=h0935bb2_0
  - gstreamer=1.14.5=h36ae1b5_0
  - harfbuzz=2.4.0=h9f30f68_2
  - icu=64.2=he1b5a44_0
  - idna=2.8=py37_1000
  - ipykernel=5.1.1=py37h5ca1d4c_0
  - ipython=7.7.0=py37h5ca1d4c_0
  - ipython_genutils=0.2.0=py_1
  - ipywidgets=7.5.1=py_0
  - itsdangerous=1.1.0=py_0
  - jedi=0.14.1=py37_0
  - jinja2=2.10.1=py_0
  - jpeg=9c=h14c3975_1001
  - jsonschema=3.0.2=py37_0
  - jupyter_client=5.3.1=py_0
  - jupyter_core=4.4.0=py_0
  - kiwisolver=1.1.0=py37hc9558a2_0
  - krb5=1.16.3=h05b26f9_1001
  - libblas=3.8.0=11_openblas
  - libcblas=3.8.0=11_openblas
  - libedit=3.1.20170329=hf8c457e_1001
  - libevent=2.1.10=h72c5cf5_0
  - libffi=3.2.1=he1b5a44_1006
  - libgcc-ng=9.1.0=hdf63c60_0
  - libgfortran-ng=7.3.0=hdf63c60_0
  - libiconv=1.15=h516909a_1005
  - liblapack=3.8.0=11_openblas
  - libopenblas=0.3.6=h6e990d7_6
  - libpng=1.6.37=hed695b0_0
  - libprotobuf=3.8.0=h8b12597_0
  - libsodium=1.0.17=h516909a_0
  - libstdcxx-ng=9.1.0=hdf63c60_0
  - libtiff=4.0.10=h57b8799_1003
  - libtool=2.4.6=h14c3975_1002
  - libuuid=2.32.1=h14c3975_1000
  - libxcb=1.13=h14c3975_1002
  - libxml2=2.9.9=hee79883_2
  - lz4-c=1.8.3=he1b5a44_1001
  - markupsafe=1.1.1=py37h14c3975_0
  - matplotlib=3.1.1=py37_1
  - matplotlib-base=3.1.1=py37he7580a8_1
  - maven=3.6.0=0
  - mistune=0.8.4=py37h14c3975_1000
  - mmh3=2.5.1=py37hf484d3e_0
  - nb_conda_kernels=2.2.2=py37_0
  - nbconvert=5.5.0=py_0
  - nbformat=4.4.0=py_1
  - ncurses=6.1=hf484d3e_1002
  - nose=1.3.7=py37_1002
  - notebook=6.0.0=py37_0
  - numpy=1.17.0=py37h95a1406_0
  - olefile=0.46=py_0
  - openjdk=9.0.7=h14c3975_1015
  - openssl=1.1.1c=h516909a_0
  - packaging=19.0=py_0
  - pandas=0.25.0=py37hb3f55d8_0
  - pandoc=2.7.3=0
  - pandocfilters=1.4.2=py_1
  - pango=1.42.4=he7ab937_0
  - parquet-cpp=1.5.1=2
  - parso=0.5.1=py_0
  - pcre=8.41=hf484d3e_1003
  - pexpect=4.7.0=py37_0
  - pickleshare=0.7.5=py37_1000
  - pillow=6.1.0=py37h6b7be26_1
  - pip=19.2.1=py37_0
  - pixman=0.38.0=h516909a_1003
  - prometheus_client=0.7.1=py_0
  - prompt_toolkit=2.0.9=py_0
  - protobuf=3.8.0=py37he6710b0_0
  - pthread-stubs=0.4=h14c3975_1001
  - ptyprocess=0.6.0=py_1001
  - pyarrow=0.14.1=py37h8b68381_0
  - pyasn1=0.4.6=py_0
  - pyasn1-modules=0.2.6=py_0
  - pycparser=2.19=py37_1
  - pydot=1.4.1=py37_1001
  - pygments=2.4.2=py_0
  - pyhamcrest=1.9.0=py_2
  - pykerberos=1.2.1=py37h14c3975_0
  - pymongo=3.8.0=py37he1b5a44_0
  - pyopenssl=19.0.0=py37_0
  - pyparsing=2.4.2=py_0
  - pyqt=5.9.2=py37hcca6a23_2
  - pyrsistent=0.15.4=py37h516909a_0
  - pysocks=1.7.0=py37_0
  - python=3.7.3=h33d41f4_1
  - python-avro=1.8.2=py37_1000
  - python-dateutil=2.8.0=py_0
  - python-hdfs=2.5.7=py_0
  - python-snappy=0.5.4=py37hee44bf9_0
  - pytz=2019.2=py_0
  - pyyaml=3.13=py37h14c3975_1001
  - pyzmq=18.0.2=py37h1768529_2
  - qt=5.9.7=h0c104cb_3
  - re2=2019.08.01=he1b5a44_0
  - readline=8.0=hf8c457e_0
  - requests=2.22.0=py37_1
  - requests-kerberos=0.12.0=py37_0
  - rsa=3.4.2=py_1
  - send2trash=1.5.0=py_0
  - setuptools=41.0.1=py37_0
  - sip=4.19.8=py37hf484d3e_1000
  - six=1.12.0=py37_1000
  - snappy=1.1.7=he1b5a44_1002
  - sqlite=3.29.0=hcee41ef_0
  - terminado=0.8.2=py37_0
  - testpath=0.4.2=py_1001
  - thrift-cpp=0.12.0=hf3afdfd_1004
  - tk=8.6.9=hed695b0_1002
  - tornado=6.0.3=py37h516909a_0
  - tqdm=4.32.2=py_0
  - traitlets=4.3.2=py37_1000
  - uriparser=0.9.3=he1b5a44_1
  - urllib3=1.25.3=py37_0
  - wcwidth=0.1.7=py_1
  - webencodings=0.5.1=py_1
  - werkzeug=0.15.5=py_0
  - wheel=0.33.4=py37_0
  - widgetsnbextension=3.5.1=py37_0
  - xorg-kbproto=1.0.7=h14c3975_1002
  - xorg-libice=1.0.10=h516909a_0
  - xorg-libsm=1.2.3=h84519dc_1000
  - xorg-libx11=1.6.8=h516909a_0
  - xorg-libxau=1.0.9=h14c3975_0
  - xorg-libxdmcp=1.1.3=h516909a_0
  - xorg-libxext=1.3.4=h516909a_0
  - xorg-libxpm=3.5.12=h14c3975_1002
  - xorg-libxrender=0.9.10=h516909a_1002
  - xorg-libxt=1.1.5=h516909a_1003
  - xorg-renderproto=0.11.1=h14c3975_1002
  - xorg-xextproto=7.3.0=h14c3975_1002
  - xorg-xproto=7.0.31=h14c3975_1007
  - xz=5.2.4=h14c3975_1001
  - yaml=0.1.7=h14c3975_1001
  - zeromq=4.3.2=he1b5a44_2
  - zlib=1.2.11=h516909a_1005
  - zstd=1.4.0=h3b9ef0a_0
  - pip:
    - crcmod==1.7
    - dill==0.2.9
    - fastavro==0.21.24
    - fasteners==0.15
    - google-apitools==0.5.30
    - google-cloud-pubsub==0.42.1
    - httplib2==0.12.0
    - mock==2.0.0
    - monotonic==1.5
    - nose-xunitmp==0.4.1
    - oauth2client==3.0.0
    - pbr==5.4.2
```

</details>

#### Description of expected behavior and the observed behavior

I started writing this issue to report a bug where I am unable to update two plots by running `push_notebook(handle1)` and `push_notebook(handle2)`. In the process of writing the bug report, I figured out that the first positional argument to `push_notebook` is actually a document and that handle has to be specified explicitly as a keyword argument: `push_notebook(handle=handle1)` and `push_notebook(handle=handle2)`. 

However, this is not a difficult mistake to make, and maybe `push_notebook` should raise an error if the first argument is a handle instead of a document? If you have reasons not to think so, please feel free to close the issue. 

#### Complete, minimal, self-contained example code that reproduces the issue

```python
#% cell 1
import math
import time

import bokeh
import numpy as np
from bokeh.io import output_notebook, push_notebook, show
from bokeh.models import DatetimeTickFormatter, HoverTool
from bokeh.plotting import figure

#% cell 2
output_notebook()

#% cell 3
def gen_plot(source):
    plot = bokeh.plotting.figure(
        plot_height=300, plot_width=800, background_fill_color="lightgrey", title="", y_range=(-1.1, 1.1)
    )
    plot.circle("x", "y", source=source)

    plot.x_range.follow = "end"
    plot.x_range.follow_interval = 100000

    plot.xaxis.major_label_orientation = math.pi / 4
    plot.xaxis.formatter = DatetimeTickFormatter(
        milliseconds=["%H:%M:%S"], seconds=["%H:%M:%S"], minsec=["%H:%M:%S"], minutes=["%H:%M:%S"]
    )
    return plot

#% cell 4
output_notebook()
source = bokeh.models.ColumnDataSource({"x": [], "y": []})
plot = gen_plot(source)
handle = bokeh.io.show(plot, notebook_handle=True)

#% cell 5
output_notebook()
source2 = bokeh.models.ColumnDataSource({"x": [], "y": []})
plot2 = gen_plot(source2)
handle2 = bokeh.io.show(plot2, notebook_handle=True)

#% cell 6
for x in range(10000):
    y = np.sin(x / 10)
    source.stream({"x": [x], "y": [y]}, rollover=20)
    source2.stream({"x": [x], "y": [y * 10]}, rollover=200)
    bokeh.io.push_notebook(handle)
    bokeh.io.push_notebook(handle2)
    time.sleep(0.01)
```



Discussion:
@bokeh/dev I'd be inclined to make all `push_notebook` args be keyword-only args starting with Bokeh 2.0 (I'm not inclined to *add* logic, that is only necessary because of Python 2, at this point)
> (I'm not inclined to add logic, that is only necessary because of Python 2, at this point)

Yes, we shouldn't put any efforts in Python 2 at this point.

Resolving patch:
diff --git a/bokeh/io/notebook.py b/bokeh/io/notebook.py
--- a/bokeh/io/notebook.py
+++ b/bokeh/io/notebook.py
@@ -184,7 +184,7 @@ def install_notebook_hook(notebook_type, load, show_doc, show_app, overwrite=Fal
         raise RuntimeError("hook for notebook type %r already exists" % notebook_type)
     _HOOKS[notebook_type] = dict(load=load, doc=show_doc, app=show_app)
 
-def push_notebook(document=None, state=None, handle=None):
+def push_notebook(*, document=None, state=None, handle=None):
     ''' Update Bokeh plots in a Jupyter notebook output cells with new data
     or property values.
 


Test patch:
diff --git a/tests/unit/bokeh/io/test_notebook__io.py b/tests/unit/bokeh/io/test_notebook__io.py
--- a/tests/unit/bokeh/io/test_notebook__io.py
+++ b/tests/unit/bokeh/io/test_notebook__io.py
@@ -82,7 +82,7 @@ def test_no_events(self, mock_comms):
         d = Document()
 
         handle = binb.CommsHandle("comms", d)
-        binb.push_notebook(d, None, handle)
+        binb.push_notebook(document=d, handle=handle)
         assert mock_comms.call_count == 0
 
     @patch('bokeh.io.notebook.CommsHandle.comms', new_callable=PropertyMock)
@@ -96,7 +96,7 @@ def test_with_events(self, mock_comms):
 
         handle = binb.CommsHandle("comms", d)
         d.title = "foo"
-        binb.push_notebook(d, None, handle)
+        binb.push_notebook(document=d, handle=handle)
         assert mock_comms.call_count > 0
         assert mock_send.call_count == 3 # sends header, metadata, then content
         assert json.loads(mock_send.call_args[0][0]) == {u"events": [{u"kind": u"TitleChanged", u"title": u"foo"}], u"references": []}