Repository: bokeh/bokeh

Issue:
Bokeh 0.12.6 incompatible with Python 2.7.9?
Hi there! I have posted this issue with the [dask.distributed project](https://github.com/dask/distributed/issues/1193#issuecomment-309802212) in which context it appeared, and I was asked to file the issue here, since it seems to be a Bokeh problem.

I have a virtual environment with the following contents:
```
> pip freeze
backports-abc==0.5
bkcharts==0.2
bokeh==0.12.6           <--------------
boto3==1.4.4
botocore==1.5.71
certifi==2017.4.17
chardet==3.0.4
click==6.7
cloudpickle==0.3.1
dask==0.15.0            <--------------
distributed==1.17.1     <--------------
docutils==0.13.1
futures==3.1.1
graphviz==0.7.1
HeapDict==1.0.0
idna==2.5
Jinja2==2.9.6
jmespath==0.9.3
locket==0.2.0
MarkupSafe==1.0
msgpack-python==0.4.8
numpy==1.13.0
pandas==0.20.2
partd==0.3.8
psutil==5.2.2
python-dateutil==2.6.0
pytz==2017.2
PyYAML==3.12
requests==2.18.1
s3fs==0.1.1
s3transfer==0.1.10
singledispatch==3.4.0.3
six==1.10.0
sortedcontainers==1.5.7
tblib==1.3.2
toolz==0.8.2
tornado==4.5.1
urllib3==1.21.1
zict==0.1.2
```
When I try to start the dask scheduler, I get the following output:
```
> dask-scheduler
distributed.scheduler - INFO - -----------------------------------------------
distributed.scheduler - INFO - Could not launch service: ('bokeh', 8787)
Traceback (most recent call last):
  File "/home/vagrant/dask_venv/local/lib/python2.7/site-packages/distributed/scheduler.py", line 404, in start_services
    service = v(self, io_loop=self.loop)
  File "/home/vagrant/dask_venv/local/lib/python2.7/site-packages/distributed/bokeh/scheduler.py", line 995, in __init__
    scheduler)))
  File "/home/vagrant/dask_venv/local/lib/python2.7/site-packages/bokeh/application/handlers/function.py", line 11, in __init__
    _check_callback(func, ('doc',))
  File "/home/vagrant/dask_venv/local/lib/python2.7/site-packages/bokeh/util/callback_manager.py", line 12, in _check_callback
    sig = signature(callback)
  File "/home/vagrant/dask_venv/local/lib/python2.7/site-packages/bokeh/util/future.py", line 85, in signature
    for name in func.keywords.keys():
AttributeError: 'NoneType' object has no attribute 'keys'
distributed.scheduler - INFO -   Scheduler at:      tcp://10.0.2.15:8786
distributed.scheduler - INFO -        http at:              0.0.0.0:9786
distributed.scheduler - INFO - Local Directory:      /tmp/scheduler-zmXtOf
distributed.scheduler - INFO - -----------------------------------------------
^Cdistributed.scheduler - INFO - End scheduler at 'tcp://:8786'
```
I can fix this problem by downgrading Bokeh to 0.12.5:
```
> pip install -U bokeh==0.12.5
...
Installing collected packages: bokeh
  Found existing installation: bokeh 0.12.6
    Uninstalling bokeh-0.12.6:
      Successfully uninstalled bokeh-0.12.6
  Running setup.py install for bokeh ... done
Successfully installed bokeh-0.12.5

> dask-scheduler
distributed.scheduler - INFO - -----------------------------------------------
distributed.scheduler - INFO -   Scheduler at:      tcp://10.0.2.15:8786
distributed.scheduler - INFO -       bokeh at:              0.0.0.0:8787
distributed.scheduler - INFO -        http at:              0.0.0.0:9786
distributed.scheduler - INFO - Local Directory:      /tmp/scheduler-U0qy1k
distributed.scheduler - INFO - -----------------------------------------------
^Cdistributed.scheduler - INFO - End scheduler at 'tcp://:8786'
```

I was able to reproduce the issue on my Debian 8 machine with Python 2.7.9. The error does _not_ occur on my Mac with Python 2.7.11. @pitrou could not reproduce the problem with Python 2.7.12.

Bokeh 0.12.6 incompatible with Python 2.7.9?
Hi there! I have posted this issue with the [dask.distributed project](https://github.com/dask/distributed/issues/1193#issuecomment-309802212) in which context it appeared, and I was asked to file the issue here, since it seems to be a Bokeh problem.

I have a virtual environment with the following contents:
```
> pip freeze
backports-abc==0.5
bkcharts==0.2
bokeh==0.12.6           <--------------
boto3==1.4.4
botocore==1.5.71
certifi==2017.4.17
chardet==3.0.4
click==6.7
cloudpickle==0.3.1
dask==0.15.0            <--------------
distributed==1.17.1     <--------------
docutils==0.13.1
futures==3.1.1
graphviz==0.7.1
HeapDict==1.0.0
idna==2.5
Jinja2==2.9.6
jmespath==0.9.3
locket==0.2.0
MarkupSafe==1.0
msgpack-python==0.4.8
numpy==1.13.0
pandas==0.20.2
partd==0.3.8
psutil==5.2.2
python-dateutil==2.6.0
pytz==2017.2
PyYAML==3.12
requests==2.18.1
s3fs==0.1.1
s3transfer==0.1.10
singledispatch==3.4.0.3
six==1.10.0
sortedcontainers==1.5.7
tblib==1.3.2
toolz==0.8.2
tornado==4.5.1
urllib3==1.21.1
zict==0.1.2
```
When I try to start the dask scheduler, I get the following output:
```
> dask-scheduler
distributed.scheduler - INFO - -----------------------------------------------
distributed.scheduler - INFO - Could not launch service: ('bokeh', 8787)
Traceback (most recent call last):
  File "/home/vagrant/dask_venv/local/lib/python2.7/site-packages/distributed/scheduler.py", line 404, in start_services
    service = v(self, io_loop=self.loop)
  File "/home/vagrant/dask_venv/local/lib/python2.7/site-packages/distributed/bokeh/scheduler.py", line 995, in __init__
    scheduler)))
  File "/home/vagrant/dask_venv/local/lib/python2.7/site-packages/bokeh/application/handlers/function.py", line 11, in __init__
    _check_callback(func, ('doc',))
  File "/home/vagrant/dask_venv/local/lib/python2.7/site-packages/bokeh/util/callback_manager.py", line 12, in _check_callback
    sig = signature(callback)
  File "/home/vagrant/dask_venv/local/lib/python2.7/site-packages/bokeh/util/future.py", line 85, in signature
    for name in func.keywords.keys():
AttributeError: 'NoneType' object has no attribute 'keys'
distributed.scheduler - INFO -   Scheduler at:      tcp://10.0.2.15:8786
distributed.scheduler - INFO -        http at:              0.0.0.0:9786
distributed.scheduler - INFO - Local Directory:      /tmp/scheduler-zmXtOf
distributed.scheduler - INFO - -----------------------------------------------
^Cdistributed.scheduler - INFO - End scheduler at 'tcp://:8786'
```
I can fix this problem by downgrading Bokeh to 0.12.5:
```
> pip install -U bokeh==0.12.5
...
Installing collected packages: bokeh
  Found existing installation: bokeh 0.12.6
    Uninstalling bokeh-0.12.6:
      Successfully uninstalled bokeh-0.12.6
  Running setup.py install for bokeh ... done
Successfully installed bokeh-0.12.5

> dask-scheduler
distributed.scheduler - INFO - -----------------------------------------------
distributed.scheduler - INFO -   Scheduler at:      tcp://10.0.2.15:8786
distributed.scheduler - INFO -       bokeh at:              0.0.0.0:8787
distributed.scheduler - INFO -        http at:              0.0.0.0:9786
distributed.scheduler - INFO - Local Directory:      /tmp/scheduler-U0qy1k
distributed.scheduler - INFO - -----------------------------------------------
^Cdistributed.scheduler - INFO - End scheduler at 'tcp://:8786'
```

I was able to reproduce the issue on my Debian 8 machine with Python 2.7.9. The error does _not_ occur on my Mac with Python 2.7.11. @pitrou could not reproduce the problem with Python 2.7.12.



Discussion:
`func` is an instance of `functools.partial` in this code and under Python 2.7.9 (actually since 2.6) has `keywords` attribute. @MathMagique, you will need to debug this and see what `func` actually is. Maybe distributed uses some custom implementation.
The explanation here is that under Python 2.7.9, the `keywords` attribute can be `None`:
```
$ python
Python 2.7.9 (default, Jun 29 2016, 13:08:31) 
[GCC 4.9.2] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import functools
>>> def f(): pass
... 
>>> g = functools.partial(f)
>>> g.keywords
>>> g.keywords is None
True
```

So I tested the same code and got different result. However, the explanation is that conda, for some reason, messed up my environment where I tested 2.7.9. In fact I was testing 2.7.12 through IPython that shouldn't be even there. 
> The explanation here is that under Python 2.7.9, the keywords attribute can be None:

I checked Python's documentation and there is nothing about this.
@mattpap, it was fixed in 2.7.10. See https://github.com/python/cpython/blob/ebbccea997e03f60fec8308f2fb9e89a11521a74/Misc/NEWS#L1465
I'm tempted to just change the Bokeh requirements to `python>=2.7.10` then. 
From an outside perspective, this seems like an additional check for `None` would be all that is required to support 2.7.9.

Please keep in mind that enterprises often use Python in server environments and are not quick to update Python to a version not bundled with the operating system. Python versions of modestly old and in some cases still current Linux distributions:
* [Debian 8 (jessie)](https://packages.debian.org/jessie/python): Python 2.7.9
* [Ubuntu 14 (trusty, )](https://packages.ubuntu.com/trusty/python): Python 2.7.5
* [Centos 7](http://centos-packages.com/7/package/python/): Python 2.7.5
* [SUSE Enterprise 12](https://www.suse.com/LinuxPackages/packageRouter.jsp?product=server&version=12&service_pack=&architecture=x86_64&package_name=python): Python 2.7.7

Ubuntu 14 (Trusty) is intended as the _new_ default for Travis CI, although they have some modifications to pull in new Python versions.
Sigh.

Is anyone here willing and able to make the small PR?
I am willing to do so. I'll get back to you with a PR when I have one.
@MathMagique brilliant, thanks!
`func` is an instance of `functools.partial` in this code and under Python 2.7.9 (actually since 2.6) has `keywords` attribute. @MathMagique, you will need to debug this and see what `func` actually is. Maybe distributed uses some custom implementation.
The explanation here is that under Python 2.7.9, the `keywords` attribute can be `None`:
```
$ python
Python 2.7.9 (default, Jun 29 2016, 13:08:31) 
[GCC 4.9.2] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import functools
>>> def f(): pass
... 
>>> g = functools.partial(f)
>>> g.keywords
>>> g.keywords is None
True
```

So I tested the same code and got different result. However, the explanation is that conda, for some reason, messed up my environment where I tested 2.7.9. In fact I was testing 2.7.12 through IPython that shouldn't be even there. 
> The explanation here is that under Python 2.7.9, the keywords attribute can be None:

I checked Python's documentation and there is nothing about this.
@mattpap, it was fixed in 2.7.10. See https://github.com/python/cpython/blob/ebbccea997e03f60fec8308f2fb9e89a11521a74/Misc/NEWS#L1465
I'm tempted to just change the Bokeh requirements to `python>=2.7.10` then. 
From an outside perspective, this seems like an additional check for `None` would be all that is required to support 2.7.9.

Please keep in mind that enterprises often use Python in server environments and are not quick to update Python to a version not bundled with the operating system. Python versions of modestly old and in some cases still current Linux distributions:
* [Debian 8 (jessie)](https://packages.debian.org/jessie/python): Python 2.7.9
* [Ubuntu 14 (trusty, )](https://packages.ubuntu.com/trusty/python): Python 2.7.5
* [Centos 7](http://centos-packages.com/7/package/python/): Python 2.7.5
* [SUSE Enterprise 12](https://www.suse.com/LinuxPackages/packageRouter.jsp?product=server&version=12&service_pack=&architecture=x86_64&package_name=python): Python 2.7.7

Ubuntu 14 (Trusty) is intended as the _new_ default for Travis CI, although they have some modifications to pull in new Python versions.
Sigh.

Is anyone here willing and able to make the small PR?
I am willing to do so. I'll get back to you with a PR when I have one.
@MathMagique brilliant, thanks!

Resolving patch:
diff --git a/bokeh/util/future.py b/bokeh/util/future.py
--- a/bokeh/util/future.py
+++ b/bokeh/util/future.py
@@ -82,8 +82,9 @@ def signature(func):
         elif isinstance(func, partial):
             sig = getargspec(func.func)
             if 'self' in sig.args: sig.args.remove('self')
-            for name in func.keywords.keys():
-                sig.args.remove(name)
+            if func.keywords is not None:
+                for name in func.keywords.keys():
+                    sig.args.remove(name)
             for val in func.args:
                 del sig.args[0]
             return sig


Test patch: